diff --git a/.codeclimate.yml b/.codeclimate.yml
index 0cefeeb427a..88d30880bd7 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -67,7 +67,7 @@ plugins:
- '.codeclimate.yml'
config:
strings:
- # Removed TODO from this list, as we want to allow TODOs in the codebase
+ - TODO
- FIXME
- HACK
- BUG
@@ -94,6 +94,5 @@ exclude_patterns:
- 'lib/tasks/create_test_accounts.rb'
- 'lib/user_flow_exporter.rb'
- 'scripts/load_testing/'
- - 'spec/'
- 'tmp/'
- 'config/initializers/jwt.rb'
diff --git a/.reek b/.reek
index d83f1dc79b1..d69ec14623d 100644
--- a/.reek
+++ b/.reek
@@ -6,6 +6,7 @@ ControlParameter:
- OpenidConnectRedirector#initialize
- NoRetryJobs#call
- PhoneFormatter#self.format
+ - Users::TwoFactorAuthenticationController#invalid_phone_number
DuplicateMethodCall:
exclude:
- ApplicationController#disable_caching
@@ -19,6 +20,7 @@ DuplicateMethodCall:
- fallback_to_english
- Idv::Proofer#load_vendors!
- Upaya::RandomTools#self.random_weighted_sample
+ - SmsController#authenticate
FeatureEnvy:
exclude:
- ActiveJob::Logging::LogSubscriber#json_for
@@ -46,6 +48,8 @@ FeatureEnvy:
- Utf8Sanitizer#remote_ip
- Idv::Proofer#validate_vendors
- PersonalKeyGenerator#create_legacy_recovery_code
+ - TwoFactorAuthenticationController#capture_analytics_for_exception
+ - Users::SessionsController#configure_permitted_parameters
InstanceVariableAssumption:
exclude:
- User
@@ -56,10 +60,11 @@ ManualDispatch:
exclude:
- EncryptedSidekiqRedis#respond_to_missing?
- CloudhsmKeyGenerator#initialize_settings
+ - Users::SessionsController#configure_permitted_parameters
NestedIterators:
exclude:
- UserFlowExporter#self.massage_html
- - TwilioService#sanitize_phone_number
+ - TwilioService::Utils#sanitize_phone_number
- ServiceProviderSeeder#run
NilCheck:
enabled: false
@@ -104,6 +109,7 @@ TooManyStatements:
- Upaya::RandomTools#self.random_weighted_sample
- UserFlowFormatter#stop
- Upaya::QueueConfig#self.choose_queue_adapter
+ - Users::TwoFactorAuthenticationController#send_code
TooManyMethods:
exclude:
- Users::ConfirmationsController
@@ -157,6 +163,7 @@ UtilityFunction:
- LocaleHelper#locale_url_param
- IdvSession#timed_out_vendor_error
- JWT::Signature#sign
+ - SmsAccountResetCancellationNotifierJob#perform
'app/controllers':
InstanceVariableAssumption:
enabled: false
diff --git a/.rubocop.yml b/.rubocop.yml
index 9cb21bca7e5..9af4903ae03 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -62,7 +62,7 @@ Metrics/ClassLength:
- app/controllers/openid_connect/authorization_controller.rb
- app/controllers/users/confirmations_controller.rb
- app/controllers/users/sessions_controller.rb
- - app/controllers/devise/two_factor_authentication_controller.rb
+ - app/controllers/users/two_factor_authentication_controller.rb
- app/decorators/service_provider_session_decorator.rb
- app/decorators/user_decorator.rb
- app/services/analytics.rb
diff --git a/Gemfile b/Gemfile
index f342eca5aca..7d125bcef3c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,7 +32,7 @@ gem 'pg'
gem 'phonelib'
gem 'pkcs11'
gem 'premailer-rails'
-gem 'proofer', github: '18F/identity-proofer-gem', tag: 'v2.5.0'
+gem 'proofer', github: '18F/identity-proofer-gem', tag: 'v2.6.1'
gem 'rack-attack'
gem 'rack-cors', require: 'rack/cors'
gem 'rack-headers_filter'
@@ -112,7 +112,7 @@ group :test do
end
group :production do
- gem 'aamva', git: 'git@github.com:18F/identity-aamva-api-client-gem', tag: 'v3.0.1'
+ gem 'aamva', git: 'git@github.com:18F/identity-aamva-api-client-gem', tag: 'v3.1.0'
gem 'equifax', git: 'git@github.com:18F/identity-equifax-api-client-gem.git', tag: 'v1.1.0'
- gem 'lexisnexis', git: 'git@github.com:18F/identity-lexisnexis-api-client-gem', tag: 'v1.0.0'
+ gem 'lexisnexis', git: 'git@github.com:18F/identity-lexisnexis-api-client-gem', tag: 'v1.1.0'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 2aca7ec3c71..9a4caf8e45d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,9 +1,9 @@
GIT
remote: git@github.com:18F/identity-aamva-api-client-gem
- revision: 015186dd86691294404229ee051cfcf9e87fb6c7
- tag: v3.0.1
+ revision: f69b0295933809057292736ed173a5a5e11b668c
+ tag: v3.1.0
specs:
- aamva (3.0.1)
+ aamva (3.1.0)
dotenv
hashie
httpi
@@ -24,10 +24,10 @@ GIT
GIT
remote: git@github.com:18F/identity-lexisnexis-api-client-gem
- revision: 2cf954c312a7e66cd24c48ccc7af8bdc72339525
- tag: v1.0.0
+ revision: d17049ab1a03d50c0cc8a272d86cf2144192fab5
+ tag: v1.1.0
specs:
- lexisnexis (1.0.0)
+ lexisnexis (1.1.0)
dotenv
typhoeus
@@ -41,10 +41,10 @@ GIT
GIT
remote: https://github.com/18F/identity-proofer-gem.git
- revision: 55191ec2124fb2b36111adf15d626d483436b74d
- tag: v2.5.0
+ revision: 875246d603bbd9b29cbc82493513f948d4e8689b
+ tag: v2.6.1
specs:
- proofer (2.5.0)
+ proofer (2.6.1)
GIT
remote: https://github.com/18F/redis-session-store.git
diff --git a/app/assets/stylesheets/components/_intl-tel-input.scss b/app/assets/stylesheets/components/_intl-tel-input.scss
index 9ce937d9b8b..680f30d7ec8 100644
--- a/app/assets/stylesheets/components/_intl-tel-input.scss
+++ b/app/assets/stylesheets/components/_intl-tel-input.scss
@@ -2,6 +2,10 @@
display: none;
}
+.intl-tel-input {
+ width: 100%;
+}
+
.no-js {
.js-intl-tel-code-select {
display: block;
diff --git a/app/controllers/account_reset/cancel_controller.rb b/app/controllers/account_reset/cancel_controller.rb
index 7c12994e22e..8cbe8659e7f 100644
--- a/app/controllers/account_reset/cancel_controller.rb
+++ b/app/controllers/account_reset/cancel_controller.rb
@@ -1,8 +1,9 @@
module AccountReset
class CancelController < ApplicationController
def cancel
- if AccountResetService.cancel_request(params[:token])
- handle_success
+ account_reset = AccountResetService.cancel_request(params[:token])
+ if account_reset
+ handle_success(account_reset.user)
else
handle_failure
end
@@ -11,9 +12,13 @@ def cancel
private
- def handle_success
- analytics.track_event(Analytics::ACCOUNT_RESET, event: :cancel, token_valid: true)
+ def handle_success(user)
+ analytics.track_event(Analytics::ACCOUNT_RESET,
+ event: :cancel, token_valid: true, user_id: user.uuid)
sign_out if current_user
+ UserMailer.account_reset_cancel(user.email).deliver_later
+ phone = user.phone
+ SmsAccountResetCancellationNotifierJob.perform_now(phone: phone) if phone.present?
flash[:success] = t('devise.two_factor_authentication.account_reset.successful_cancel')
end
diff --git a/app/controllers/account_reset/delete_account_controller.rb b/app/controllers/account_reset/delete_account_controller.rb
index b97d9c5a2b3..d6a13530dc8 100644
--- a/app/controllers/account_reset/delete_account_controller.rb
+++ b/app/controllers/account_reset/delete_account_controller.rb
@@ -7,8 +7,10 @@ class DeleteAccountController < ApplicationController
def show; end
def delete
- analytics.track_event(Analytics::ACCOUNT_RESET, event: :delete, token_valid: true)
- email = reset_session_and_set_email
+ user = @account_reset_request.user
+ analytics.track_event(Analytics::ACCOUNT_RESET,
+ event: :delete, token_valid: true, user_id: user.uuid)
+ email = reset_session_and_set_email(user)
UserMailer.account_reset_complete(email).deliver_later
redirect_to account_reset_confirm_delete_account_url
end
@@ -19,8 +21,7 @@ def check_feature_enabled
redirect_to root_url unless FeatureManagement.account_reset_enabled?
end
- def reset_session_and_set_email
- user = @account_reset_request.user
+ def reset_session_and_set_email(user)
email = user.email
user.destroy!
sign_out
diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb
index 0b99bc2ace5..0eb7e428faa 100644
--- a/app/controllers/account_reset/request_controller.rb
+++ b/app/controllers/account_reset/request_controller.rb
@@ -28,10 +28,13 @@ def reset_session_with_email
end
def send_notifications
- SmsAccountResetNotifierJob.perform_now(
- phone: current_user.phone,
- cancel_token: current_user.account_reset_request.request_token
- )
+ phone = current_user.phone
+ if phone
+ SmsAccountResetNotifierJob.perform_now(
+ phone: phone,
+ cancel_token: current_user.account_reset_request.request_token
+ )
+ end
UserMailer.account_reset_request(current_user).deliver_later
end
diff --git a/app/controllers/concerns/user_session_context.rb b/app/controllers/concerns/user_session_context.rb
index e7f58b53970..ed40c55796b 100644
--- a/app/controllers/concerns/user_session_context.rb
+++ b/app/controllers/concerns/user_session_context.rb
@@ -5,7 +5,6 @@ def context
user_session[:context] || DEFAULT_CONTEXT
end
- # TODO: Figure out better names for this and the method below
def initial_authentication_context?
context == DEFAULT_CONTEXT
end
diff --git a/app/controllers/sms_controller.rb b/app/controllers/sms_controller.rb
new file mode 100644
index 00000000000..2d11844de64
--- /dev/null
+++ b/app/controllers/sms_controller.rb
@@ -0,0 +1,74 @@
+class SmsController < ApplicationController
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
+ include SecureHeadersConcern
+
+ # Twilio supports HTTP Basic Auth for request URL
+ # https://www.twilio.com/docs/usage/security
+ before_action :authenticate
+
+ # Disable CSRF check
+ skip_before_action :verify_authenticity_token, only: [:receive]
+
+ def receive
+ signature = request.headers[TwilioService::Sms::Request::SIGNATURE_HEADER]
+ message = TwilioService::Sms::Request.new(request.url, params, signature)
+
+ handle_result(message, SmsForm.new(message).submit)
+ end
+
+ private
+
+ def handle_result(message, result)
+ if result.success?
+ process_success(message, result)
+ else
+ process_failure(result)
+ end
+ end
+
+ def process_success(message, result)
+ response = TwilioService::Sms::Response.new(message)
+ SmsReplySenderJob.perform_later(response.reply)
+
+ analytics.track_event(
+ Analytics::TWILIO_SMS_INBOUND_MESSAGE_RECEIVED,
+ result.to_h
+ )
+
+ head :accepted
+ end
+
+ def process_failure(result)
+ analytics.track_event(
+ Analytics::TWILIO_SMS_INBOUND_MESSAGE_VALIDATION_FAILED,
+ result.to_h
+ )
+
+ head :forbidden
+ end
+
+ # `http_basic_authenticate_with name` had issues related to testing, so using
+ # this method with a before action instead. (The former is a shortcut for the
+ # following, which is called internally by Rails.)
+ def authenticate
+ env = Figaro.env
+
+ head :unauthorized unless auth_configured?(env)
+
+ authenticate_or_request_with_http_basic do |username, password|
+ # This comparison uses & so that it doesn't short circuit and
+ # uses `secure_compare` so that length information
+ # isn't leaked.
+ ActiveSupport::SecurityUtils.secure_compare(
+ username, env.twilio_http_basic_auth_username
+ ) & ActiveSupport::SecurityUtils.secure_compare(
+ password, env.twilio_http_basic_auth_password
+ )
+ end
+ end
+
+ def auth_configured?(env)
+ env.twilio_http_basic_auth_username.present? &&
+ env.twilio_http_basic_auth_password.present?
+ end
+end
diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb
new file mode 100644
index 00000000000..87bf581bdc7
--- /dev/null
+++ b/app/controllers/two_factor_authentication/options_controller.rb
@@ -0,0 +1,46 @@
+module TwoFactorAuthentication
+ class OptionsController < ApplicationController
+ include TwoFactorAuthenticatable
+
+ def index
+ @two_factor_options_form = TwoFactorLoginOptionsForm.new(current_user)
+ @presenter = two_factor_options_presenter
+ analytics.track_event(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST_VISIT)
+ end
+
+ def create
+ @two_factor_options_form = TwoFactorLoginOptionsForm.new(current_user)
+ result = @two_factor_options_form.submit(two_factor_options_form_params)
+ analytics.track_event(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST, result.to_h)
+
+ if result.success?
+ process_valid_form
+ else
+ @presenter = two_factor_options_presenter
+ render :index
+ end
+ end
+
+ private
+
+ def two_factor_options_presenter
+ TwoFactorLoginOptionsPresenter.new(current_user, view_context, current_sp)
+ end
+
+ def process_valid_form
+ factor_to_url = {
+ 'voice' => otp_send_url(otp_delivery_selection_form: { otp_delivery_preference: 'voice' }),
+ 'personal_key' => login_two_factor_personal_key_url,
+ 'sms' => otp_send_url(otp_delivery_selection_form: { otp_delivery_preference: 'sms' }),
+ 'auth_app' => login_two_factor_authenticator_url,
+ 'piv_cac' => login_two_factor_piv_cac_url,
+ }
+ url = factor_to_url[@two_factor_options_form.selection]
+ redirect_to url if url
+ end
+
+ def two_factor_options_form_params
+ params.require(:two_factor_options_form).permit(:selection)
+ end
+ end
+end
diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb
index 8811fcaf4ed..033cbf37d18 100644
--- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb
@@ -8,7 +8,7 @@ def show
analytics.track_event(
Analytics::MULTI_FACTOR_AUTH_ENTER_PERSONAL_KEY_VISIT, context: context
)
-
+ @presenter = TwoFactorAuthCode::PersonalKeyPresenter.new
@personal_key_form = PersonalKeyForm.new(current_user)
end
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index 31a53946858..3372aae0914 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -9,6 +9,7 @@ class SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:new]
before_action :check_user_needs_redirect, only: [:new]
before_action :apply_secure_headers_override, only: [:new]
+ before_action :configure_permitted_parameters, only: [:new]
def new
analytics.track_event(
@@ -48,6 +49,12 @@ def timeout
private
+ def configure_permitted_parameters
+ devise_parameter_sanitizer.permit(:sign_in) do |user_params|
+ user_params.permit(:email) if user_params.respond_to?(:permit)
+ end
+ end
+
def redirect_to_signin
controller_info = 'users/sessions#create'
analytics.track_event(Analytics::INVALID_AUTHENTICITY_TOKEN, controller: controller_info)
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
index 2c93a0fd861..b24e0eeaf25 100644
--- a/app/controllers/users/two_factor_authentication_controller.rb
+++ b/app/controllers/users/two_factor_authentication_controller.rb
@@ -4,6 +4,7 @@ class TwoFactorAuthenticationController < ApplicationController
before_action :check_remember_device_preference
+ # rubocop:disable Metrics/MethodLength
def show
if current_user.piv_cac_enabled?
redirect_to login_two_factor_piv_cac_url
@@ -14,7 +15,10 @@ def show
else
redirect_to two_factor_options_url
end
+ rescue Twilio::REST::RestError, PhoneVerification::VerifyError => exception
+ invalid_phone_number(exception, action: 'show')
end
+ # rubocop:enable Metrics/MethodLength
def send_code
result = otp_delivery_selection_form.submit(delivery_params)
@@ -22,11 +26,12 @@ def send_code
if result.success?
handle_valid_otp_delivery_preference(user_selected_otp_delivery_preference)
+ update_otp_delivery_preference_if_needed
else
handle_invalid_otp_delivery_preference(result)
end
rescue Twilio::REST::RestError, PhoneVerification::VerifyError => exception
- invalid_phone_number(exception)
+ invalid_phone_number(exception, action: 'send_code')
end
private
@@ -44,19 +49,56 @@ def validate_otp_delivery_preference_and_send_code
end
end
+ def update_otp_delivery_preference_if_needed
+ OtpDeliveryPreferenceUpdater.new(
+ user: current_user,
+ preference: delivery_params[:otp_delivery_preference],
+ context: otp_delivery_selection_form.context
+ ).call
+ end
+
def handle_invalid_otp_delivery_preference(result)
flash[:error] = result.errors[:phone].first
preference = current_user.otp_delivery_preference
redirect_to login_two_factor_url(otp_delivery_preference: preference)
end
- def invalid_phone_number(exception)
- code = exception.code
- analytics.track_event(
- Analytics::TWILIO_PHONE_VALIDATION_FAILED, error: exception.message, code: code
+ def invalid_phone_number(exception, action:)
+ capture_analytics_for_exception(exception)
+
+ if action == 'show'
+ redirect_to_otp_verification_with_error
+ else
+ flash[:error] = error_message(exception.code)
+ redirect_back(fallback_location: account_url)
+ end
+ end
+
+ def redirect_to_otp_verification_with_error
+ flash[:error] = t('errors.messages.phone_unsupported')
+ redirect_to login_two_factor_url(
+ otp_delivery_preference: current_user.otp_delivery_preference, reauthn: reauthn?
)
- flash[:error] = error_message(code)
- redirect_back(fallback_location: account_url)
+ end
+
+ # rubocop:disable Metrics/MethodLength
+ def capture_analytics_for_exception(exception)
+ attributes = {
+ error: exception.message,
+ code: exception.code,
+ context: context,
+ country: parsed_phone.country,
+ }
+ if exception.is_a?(PhoneVerification::VerifyError)
+ attributes[:status] = exception.status
+ attributes[:response] = exception.response
+ end
+ analytics.track_event(Analytics::TWILIO_PHONE_VALIDATION_FAILED, attributes)
+ end
+ # rubocop:enable Metrics/MethodLength
+
+ def parsed_phone
+ @parsed_phone ||= Phonelib.parse(phone_to_deliver_to)
end
def error_message(code)
@@ -68,7 +110,9 @@ def twilio_errors
end
def otp_delivery_selection_form
- OtpDeliverySelectionForm.new(current_user, phone_to_deliver_to, context)
+ @otp_delivery_selection_form ||= OtpDeliverySelectionForm.new(
+ current_user, phone_to_deliver_to, context
+ )
end
def reauthn_param
diff --git a/app/forms/openid_connect_token_form.rb b/app/forms/openid_connect_token_form.rb
index 4d690e2fa13..3afd4f57718 100644
--- a/app/forms/openid_connect_token_form.rb
+++ b/app/forms/openid_connect_token_form.rb
@@ -96,7 +96,6 @@ def validate_client_assertion
sub: client_id, verify_sub: true)
validate_aud_claim(payload)
rescue JWT::DecodeError => err
- # TODO: i18n these JWT gem error messages
errors.add(:client_assertion, err.message)
end
diff --git a/app/forms/otp_delivery_selection_form.rb b/app/forms/otp_delivery_selection_form.rb
index 4971439a99f..75b34f515f2 100644
--- a/app/forms/otp_delivery_selection_form.rb
+++ b/app/forms/otp_delivery_selection_form.rb
@@ -2,7 +2,7 @@ class OtpDeliverySelectionForm
include ActiveModel::Model
include OtpDeliveryPreferenceValidator
- attr_reader :otp_delivery_preference, :phone
+ attr_reader :otp_delivery_preference, :phone, :context
validates :otp_delivery_preference, inclusion: { in: %w[sms voice] }
validates :phone, presence: true
@@ -20,7 +20,6 @@ def submit(params)
@success = valid?
change_otp_delivery_preference_to_sms if unsupported_phone?
- update_otp_delivery_preference if should_update_user?
FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes)
end
@@ -29,7 +28,7 @@ def submit(params)
attr_writer :otp_delivery_preference
attr_accessor :resend
- attr_reader :success, :user, :context
+ attr_reader :success, :user
def change_otp_delivery_preference_to_sms
user_attributes = { otp_delivery_preference: 'sms' }
@@ -43,24 +42,6 @@ def unsupported_phone?
error_messages[:phone].first != I18n.t('errors.messages.missing_field')
end
- def update_otp_delivery_preference
- user_attributes = { otp_delivery_preference: otp_delivery_preference }
- UpdateUser.new(user: user, attributes: user_attributes).call
- end
-
- def idv_context?
- context == 'idv'
- end
-
- def otp_delivery_preference_changed?
- otp_delivery_preference != user.otp_delivery_preference
- end
-
- def should_update_user?
- return false if unsupported_phone?
- success && otp_delivery_preference_changed? && !idv_context?
- end
-
def extra_analytics_attributes
{
otp_delivery_preference: otp_delivery_preference,
diff --git a/app/forms/sms_form.rb b/app/forms/sms_form.rb
new file mode 100644
index 00000000000..830e84bc32c
--- /dev/null
+++ b/app/forms/sms_form.rb
@@ -0,0 +1,28 @@
+class SmsForm
+ include ActiveModel::Model
+
+ validate :validate_message
+
+ def initialize(message)
+ @message = message
+ end
+
+ def submit
+ success = valid?
+
+ FormResponse.new(
+ success: success,
+ errors: errors.messages,
+ extra: message.extra_analytics_attributes
+ )
+ end
+
+ private
+
+ attr_reader :message
+
+ def validate_message
+ return if message.valid?
+ errors.add :base, :twilio_inbound_sms_invalid
+ end
+end
diff --git a/app/forms/two_factor_login_options_form.rb b/app/forms/two_factor_login_options_form.rb
new file mode 100644
index 00000000000..b37f8afb9b7
--- /dev/null
+++ b/app/forms/two_factor_login_options_form.rb
@@ -0,0 +1,30 @@
+class TwoFactorLoginOptionsForm
+ include ActiveModel::Model
+
+ attr_reader :selection
+
+ validates :selection, inclusion: { in: %w[voice sms auth_app piv_cac personal_key] }
+
+ def initialize(user)
+ self.user = user
+ end
+
+ def submit(params)
+ self.selection = params[:selection]
+
+ success = valid?
+
+ FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes)
+ end
+
+ private
+
+ attr_accessor :user
+ attr_writer :selection
+
+ def extra_analytics_attributes
+ {
+ selection: selection,
+ }
+ end
+end
diff --git a/app/javascript/packs/personal-key-page-controller.js b/app/javascript/packs/personal-key-page-controller.js
index c3122b67f7c..3e0f67e42b5 100644
--- a/app/javascript/packs/personal-key-page-controller.js
+++ b/app/javascript/packs/personal-key-page-controller.js
@@ -24,7 +24,6 @@ const personalKey = scrapePersonalKey();
// The following methods are strictly fallbacks for IE < 11. There is limited
// support for HTML5 validation attributes in those browsers
-// TODO: Potentially investigate readding client-side JS errors in a robust way
function setInvalidHTML() {
if (isInvalidForm) return;
diff --git a/app/jobs/sms_account_reset_cancellation_notifier_job.rb b/app/jobs/sms_account_reset_cancellation_notifier_job.rb
new file mode 100644
index 00000000000..d8f96b51e87
--- /dev/null
+++ b/app/jobs/sms_account_reset_cancellation_notifier_job.rb
@@ -0,0 +1,13 @@
+class SmsAccountResetCancellationNotifierJob < ApplicationJob
+ queue_as :sms
+
+ def perform(phone:)
+ TwilioService::Utils.new.send_sms(
+ to: phone,
+ body: I18n.t(
+ 'jobs.sms_account_reset_cancel_job.message',
+ app: APP_NAME
+ )
+ )
+ end
+end
diff --git a/app/jobs/sms_account_reset_notifier_job.rb b/app/jobs/sms_account_reset_notifier_job.rb
index f4584572f27..43d5045c07e 100644
--- a/app/jobs/sms_account_reset_notifier_job.rb
+++ b/app/jobs/sms_account_reset_notifier_job.rb
@@ -3,7 +3,7 @@ class SmsAccountResetNotifierJob < ApplicationJob
include Rails.application.routes.url_helpers
def perform(phone:, cancel_token:)
- TwilioService.new.send_sms(
+ TwilioService::Utils.new.send_sms(
to: phone,
body: I18n.t(
'jobs.sms_account_reset_notifier_job.message',
diff --git a/app/jobs/sms_otp_sender_job.rb b/app/jobs/sms_otp_sender_job.rb
index 073363f2529..157e8d90c99 100644
--- a/app/jobs/sms_otp_sender_job.rb
+++ b/app/jobs/sms_otp_sender_job.rb
@@ -5,7 +5,7 @@ class SmsOtpSenderJob < ApplicationJob
def perform(code:, phone:, otp_created_at:, locale: nil)
return unless otp_valid?(otp_created_at)
- if us_number?
+ if programmable_sms_number?
send_sms_via_twilio_rest_api
else
send_sms_via_twilio_verify_api(locale)
@@ -24,7 +24,7 @@ def phone
end
def send_sms_via_twilio_rest_api
- TwilioService.new.send_sms(
+ TwilioService::Utils.new.send_sms(
to: phone,
body: I18n.t(
'jobs.sms_otp_sender_job.message',
@@ -37,8 +37,9 @@ def send_sms_via_twilio_verify_api(locale)
PhoneVerification.new(phone: phone, locale: locale, code: code).send_sms
end
- def us_number?
- Phonelib.parse(phone).country == 'US'
+ def programmable_sms_number?
+ programmable_sms_countries = Figaro.env.programmable_sms_countries.split(',')
+ programmable_sms_countries.include?(Phonelib.parse(phone).country)
end
def otp_valid?(otp_created_at)
diff --git a/app/jobs/sms_reply_sender_job.rb b/app/jobs/sms_reply_sender_job.rb
new file mode 100644
index 00000000000..b1d4b6d7643
--- /dev/null
+++ b/app/jobs/sms_reply_sender_job.rb
@@ -0,0 +1,13 @@
+class SmsReplySenderJob < ApplicationJob
+ queue_as :sms
+
+ def perform(params)
+ send_reply(params)
+ end
+
+ private
+
+ def send_reply(params)
+ TwilioService::Utils.new.send_sms(params)
+ end
+end
diff --git a/app/jobs/voice_otp_sender_job.rb b/app/jobs/voice_otp_sender_job.rb
index b9d17f696cb..65aad329926 100644
--- a/app/jobs/voice_otp_sender_job.rb
+++ b/app/jobs/voice_otp_sender_job.rb
@@ -10,7 +10,7 @@ class VoiceOtpSenderJob < ApplicationJob
# writing, we are only using Verify for non-US SMS, but we might expand
# to Voice later.
def perform(code:, phone:, otp_created_at:, locale: nil)
- send_otp(TwilioService.new, code, phone) if otp_valid?(otp_created_at)
+ send_otp(TwilioService::Utils.new, code, phone) if otp_valid?(otp_created_at)
end
# rubocop:enable Lint/UnusedMethodArgument
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 72ff09e8954..2c6aa0e24e2 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -42,4 +42,8 @@ def account_reset_granted(user, account_reset)
def account_reset_complete(email)
mail(to: email, subject: t('user_mailer.account_reset_complete.subject'))
end
+
+ def account_reset_cancel(email)
+ mail(to: email, subject: t('user_mailer.account_reset_cancel.subject'))
+ end
end
diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb
index 95582e98bae..83eab07e127 100644
--- a/app/models/anonymous_user.rb
+++ b/app/models/anonymous_user.rb
@@ -6,4 +6,8 @@ def uuid
def second_factor_locked_at
nil
end
+
+ def phone
+ nil
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3208419e1d5..d7011b09973 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -101,12 +101,7 @@ def last_identity
end
def active_identities
- identities.where(
- 'session_uuid IS NOT ?',
- nil
- ).order(
- last_authenticated_at: :asc
- ) || []
+ identities.where('session_uuid IS NOT ?', nil).order(last_authenticated_at: :asc) || []
end
def multiple_identities?
diff --git a/app/policies/auth_app_login_option_policy.rb b/app/policies/auth_app_login_option_policy.rb
new file mode 100644
index 00000000000..93a07ee4fd0
--- /dev/null
+++ b/app/policies/auth_app_login_option_policy.rb
@@ -0,0 +1,13 @@
+class AuthAppLoginOptionPolicy
+ def initialize(user)
+ @user = user
+ end
+
+ def configured?
+ !user.otp_secret_key.nil?
+ end
+
+ private
+
+ attr_reader :user
+end
diff --git a/app/policies/personal_key_login_option_policy.rb b/app/policies/personal_key_login_option_policy.rb
new file mode 100644
index 00000000000..05418eee4ed
--- /dev/null
+++ b/app/policies/personal_key_login_option_policy.rb
@@ -0,0 +1,13 @@
+class PersonalKeyLoginOptionPolicy
+ def initialize(user)
+ @user = user
+ end
+
+ def configured?
+ user.personal_key.present?
+ end
+
+ private
+
+ attr_reader :user
+end
diff --git a/app/policies/piv_cac_login_option_policy.rb b/app/policies/piv_cac_login_option_policy.rb
new file mode 100644
index 00000000000..aca26fe7c5d
--- /dev/null
+++ b/app/policies/piv_cac_login_option_policy.rb
@@ -0,0 +1,13 @@
+class PivCacLoginOptionPolicy
+ def initialize(user)
+ @user = user
+ end
+
+ def configured?
+ FeatureManagement.piv_cac_enabled? && user.x509_dn_uuid.present?
+ end
+
+ private
+
+ attr_reader :user
+end
diff --git a/app/policies/sms_login_option_policy.rb b/app/policies/sms_login_option_policy.rb
new file mode 100644
index 00000000000..53657192910
--- /dev/null
+++ b/app/policies/sms_login_option_policy.rb
@@ -0,0 +1,13 @@
+class SmsLoginOptionPolicy
+ def initialize(user)
+ @user = user
+ end
+
+ def configured?
+ user.phone.present?
+ end
+
+ private
+
+ attr_reader :user
+end
diff --git a/app/policies/voice_login_option_policy.rb b/app/policies/voice_login_option_policy.rb
new file mode 100644
index 00000000000..23ca5d4b8c4
--- /dev/null
+++ b/app/policies/voice_login_option_policy.rb
@@ -0,0 +1,18 @@
+class VoiceLoginOptionPolicy
+ def initialize(user)
+ @user = user
+ end
+
+ def configured?
+ user_has_a_phone_number_that_we_can_call?
+ end
+
+ private
+
+ attr_reader :user
+
+ def user_has_a_phone_number_that_we_can_call?
+ phone = user.phone
+ phone.present? && !PhoneNumberCapabilities.new(phone).sms_only?
+ end
+end
diff --git a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb
index ee9e22e0f5c..082527b1dd2 100644
--- a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb
+++ b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb
@@ -11,12 +11,8 @@ def help_text
tooltip: view.tooltip(t('tooltips.authentication_app')))
end
- def fallback_links
- [
- otp_fallback_options,
- piv_cac_link,
- personal_key_link,
- ].compact
+ def fallback_question
+ t('two_factor_authentication.totp_fallback.question')
end
def cancel_link
@@ -31,30 +27,5 @@ def cancel_link
private
attr_reader :user_email, :two_factor_authentication_method, :phone_enabled
-
- def otp_fallback_options
- return unless phone_enabled
- t(
- 'devise.two_factor_authentication.totp_fallback.text_html',
- sms_link: sms_link,
- voice_link: voice_link
- )
- end
-
- def sms_link
- view.link_to(
- t('devise.two_factor_authentication.totp_fallback.sms_link_text'),
- otp_send_path(locale: LinkLocaleResolver.locale, otp_delivery_selection_form:
- { otp_delivery_preference: 'sms' })
- )
- end
-
- def voice_link
- view.link_to(
- t('devise.two_factor_authentication.totp_fallback.voice_link_text'),
- otp_send_path(locale: LinkLocaleResolver.locale, otp_delivery_selection_form:
- { otp_delivery_preference: 'voice' })
- )
- end
end
end
diff --git a/app/presenters/two_factor_auth_code/personal_key_presenter.rb b/app/presenters/two_factor_auth_code/personal_key_presenter.rb
new file mode 100644
index 00000000000..428307a1095
--- /dev/null
+++ b/app/presenters/two_factor_auth_code/personal_key_presenter.rb
@@ -0,0 +1,13 @@
+module TwoFactorAuthCode
+ class PersonalKeyPresenter < TwoFactorAuthCode::GenericDeliveryPresenter
+ def initialize; end
+
+ def help_text
+ ''
+ end
+
+ def fallback_question
+ t('two_factor_authentication.personal_key_fallback.question')
+ end
+ end
+end
diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
index 47c4708c4a1..286f71eb134 100644
--- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
+++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
@@ -1,5 +1,9 @@
module TwoFactorAuthCode
class PhoneDeliveryPresenter < TwoFactorAuthCode::GenericDeliveryPresenter
+ attr_reader(
+ :otp_delivery_preference
+ )
+
def header
t('devise.two_factor_authentication.header_text')
end
@@ -10,19 +14,19 @@ def phone_number_message
expiration: Figaro.env.otp_valid_for)
end
+ def fallback_question
+ t('two_factor_authentication.phone_fallback.question')
+ end
+
def help_text
- t("instructions.mfa.#{otp_delivery_preference}.confirm_code_html",
- resend_code_link: resend_code_link)
+ ''
end
- def fallback_links
- [
- otp_fallback_options,
- update_phone_link,
- piv_cac_option,
- personal_key_link,
- account_reset_link,
- ].compact
+ def update_phone_link
+ return unless unconfirmed_phone
+
+ link = view.link_to(t('forms.two_factor.try_again'), reenter_phone_number_path)
+ t('instructions.mfa.wrong_number_html', link: link)
end
def cancel_link
@@ -43,95 +47,15 @@ def cancel_link
:reenter_phone_number_path,
:phone_number,
:unconfirmed_phone,
- :otp_delivery_preference,
:account_reset_token,
:confirmation_for_phone_change,
:voice_otp_delivery_unsupported,
:confirmation_for_idv
)
- def otp_fallback_options
- if totp_enabled
- otp_fallback_options_with_totp
- elsif !voice_otp_delivery_unsupported
- safe_join([phone_fallback_link, '.'])
- end
- end
-
- def otp_fallback_options_with_totp
- if voice_otp_delivery_unsupported
- safe_join([auth_app_fallback_tag, '.'])
- else
- safe_join([phone_fallback_link, auth_app_fallback_link])
- end
- end
-
- def update_phone_link
- return unless unconfirmed_phone
-
- link = view.link_to(t('forms.two_factor.try_again'), reenter_phone_number_path)
- t('instructions.mfa.wrong_number_html', link: link)
- end
-
def account_reset_link
return if unconfirmed_phone || !FeatureManagement.account_reset_enabled?
account_reset_or_cancel_link
end
-
- def account_reset_or_cancel_link
- if account_reset_token
- t('devise.two_factor_authentication.account_reset.pending_html', cancel_link:
- view.link_to(t('devise.two_factor_authentication.account_reset.cancel_link'),
- account_reset_cancel_url(token: account_reset_token)))
- else
- t('devise.two_factor_authentication.account_reset.text_html', link:
- view.link_to(t('devise.two_factor_authentication.account_reset.link'),
- account_reset_request_path(locale: LinkLocaleResolver.locale)))
- end
- end
-
- def phone_fallback_link
- t(fallback_instructions, link: phone_link_tag)
- end
-
- def phone_link_tag
- view.link_to(
- t("links.two_factor_authentication.#{fallback_method}"),
- otp_send_path(locale: LinkLocaleResolver.locale, otp_delivery_selection_form:
- { otp_delivery_preference: fallback_method })
- )
- end
-
- def auth_app_fallback_link
- t('links.phone_confirmation.auth_app_fallback_html', link: auth_app_fallback_tag)
- end
-
- def auth_app_fallback_tag
- view.link_to(
- t('links.two_factor_authentication.app'),
- login_two_factor_authenticator_path(locale: LinkLocaleResolver.locale)
- )
- end
-
- def fallback_instructions
- "instructions.mfa.#{otp_delivery_preference}.fallback_html"
- end
-
- def fallback_method
- if otp_delivery_preference == 'voice'
- 'sms'
- elsif otp_delivery_preference == 'sms'
- 'voice'
- end
- end
-
- def resend_code_link
- view.link_to(
- t("links.two_factor_authentication.resend_code.#{otp_delivery_preference}"),
- otp_send_path(locale: LinkLocaleResolver.locale,
- otp_delivery_selection_form:
- { otp_delivery_preference: otp_delivery_preference, resend: true })
- )
- end
end
end
diff --git a/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb b/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb
index 8efbed8b36f..719781d08ba 100644
--- a/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb
+++ b/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb
@@ -17,14 +17,6 @@ def piv_cac_capture_text
t('forms.piv_cac_mfa.submit')
end
- def fallback_links
- [
- otp_fallback_options,
- auth_app_fallback,
- personal_key_link,
- ].compact
- end
-
def cancel_link
locale = LinkLocaleResolver.locale
if reauthn
@@ -38,42 +30,12 @@ def piv_cac_service_link
redirect_to_piv_cac_service_url
end
- private
-
- attr_reader :user_email, :two_factor_authentication_method, :totp_enabled, :phone_enabled
-
- def otp_fallback_options
- return unless phone_enabled
- t(
- 'devise.two_factor_authentication.totp_fallback.text_html',
- sms_link: sms_link,
- voice_link: voice_link
- )
- end
-
- def sms_link
- view.link_to(
- t('devise.two_factor_authentication.totp_fallback.sms_link_text'),
- login_two_factor_path(locale: LinkLocaleResolver.locale, otp_delivery_preference: 'sms')
- )
- end
-
- def voice_link
- view.link_to(
- t('devise.two_factor_authentication.totp_fallback.voice_link_text'),
- login_two_factor_path(locale: LinkLocaleResolver.locale, otp_delivery_preference: 'voice')
- )
+ def fallback_question
+ t('two_factor_authentication.piv_cac_fallback.question')
end
- def auth_app_fallback
- safe_join([auth_app_fallback_tag, '.']) if totp_enabled
- end
+ private
- def auth_app_fallback_tag
- view.link_to(
- t('links.two_factor_authentication.app'),
- login_two_factor_authenticator_path(locale: LinkLocaleResolver.locale)
- )
- end
+ attr_reader :user_email, :two_factor_authentication_method, :totp_enabled, :phone_enabled
end
end
diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb
new file mode 100644
index 00000000000..0775f3b99c1
--- /dev/null
+++ b/app/presenters/two_factor_login_options_presenter.rb
@@ -0,0 +1,79 @@
+class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresenter
+ include ActionView::Helpers::TranslationHelper
+
+ POSSIBLE_OPTIONS = %i[sms voice auth_app piv_cac personal_key].freeze
+ POLICIES = {
+ sms: SmsLoginOptionPolicy,
+ voice: VoiceLoginOptionPolicy,
+ auth_app: AuthAppLoginOptionPolicy,
+ piv_cac: PivCacLoginOptionPolicy,
+ personal_key: PersonalKeyLoginOptionPolicy,
+ }.freeze
+
+ attr_reader :current_user
+
+ def initialize(current_user, view, service_provider)
+ @current_user = current_user
+ @view = view
+ @service_provider = service_provider
+ end
+
+ def title
+ t('two_factor_authentication.login_options_title')
+ end
+
+ def heading
+ t('two_factor_authentication.login_options_title')
+ end
+
+ def info
+ t('two_factor_authentication.login_intro')
+ end
+
+ def label
+ ''
+ end
+
+ def options
+ configured_2fa_types.map do |type|
+ OpenStruct.new(
+ type: type,
+ label: t("two_factor_authentication.login_options.#{type}"),
+ info: t("two_factor_authentication.login_options.#{type}_info"),
+ selected: type == configured_2fa_types[0]
+ )
+ end
+ end
+
+ def account_reset_or_cancel_link
+ account_reset_token ? account_reset_cancel_link : account_reset_link
+ end
+
+ private
+
+ def account_reset_link
+ t('devise.two_factor_authentication.account_reset.text_html',
+ link: @view.link_to(
+ t('devise.two_factor_authentication.account_reset.link'),
+ account_reset_request_path(locale: LinkLocaleResolver.locale)
+ ))
+ end
+
+ def account_reset_cancel_link
+ t('devise.two_factor_authentication.account_reset.pending_html',
+ cancel_link: @view.link_to(
+ t('devise.two_factor_authentication.account_reset.cancel_link'),
+ account_reset_cancel_url(token: account_reset_token)
+ ))
+ end
+
+ def account_reset_token
+ current_user&.account_reset_request&.request_token
+ end
+
+ def configured_2fa_types
+ POSSIBLE_OPTIONS.each_with_object([]) do |option, result|
+ result << option if POLICIES[option].new(@current_user).configured?
+ end
+ end
+end
diff --git a/app/services/account_reset_service.rb b/app/services/account_reset_service.rb
index 5bb429cb16d..901bdc5ee18 100644
--- a/app/services/account_reset_service.rb
+++ b/app/services/account_reset_service.rb
@@ -18,6 +18,7 @@ def self.cancel_request(token)
account_reset.update(cancelled_at: Time.zone.now,
request_token: nil,
granted_token: nil)
+ account_reset
end
def self.report_fraud(token)
@@ -66,10 +67,6 @@ def self.send_notifications_with_sql(users_sql)
def self.reset_and_notify(arr)
user = arr.user
return false unless AccountResetService.new(user).grant_request
- SmsAccountResetNotifierJob.perform_now(
- phone: user.phone,
- cancel_token: arr.request_token
- )
UserMailer.account_reset_granted(user, arr).deliver_later
true
end
diff --git a/app/services/analytics.rb b/app/services/analytics.rb
index afca14abc63..485e9f24b74 100644
--- a/app/services/analytics.rb
+++ b/app/services/analytics.rb
@@ -78,6 +78,8 @@ def browser
MULTI_FACTOR_AUTH_ENTER_OTP_VISIT = 'Multi-Factor Authentication: enter OTP visited'.freeze
MULTI_FACTOR_AUTH_ENTER_PERSONAL_KEY_VISIT = 'Multi-Factor Authentication: enter personal key visited'.freeze
MULTI_FACTOR_AUTH_MAX_ATTEMPTS = 'Multi-Factor Authentication: max attempts reached'.freeze
+ MULTI_FACTOR_AUTH_OPTION_LIST = 'Multi-Factor Authentication: option list'.freeze
+ MULTI_FACTOR_AUTH_OPTION_LIST_VISIT = 'Multi-Factor Authentication: option list visited'.freeze
MULTI_FACTOR_AUTH_PHONE_SETUP = 'Multi-Factor Authentication: phone setup'.freeze
MULTI_FACTOR_AUTH_MAX_SENDS = 'Multi-Factor Authentication: max otp sends reached'.freeze
OPENID_CONNECT_BEARER_TOKEN = 'OpenID Connect: bearer token authentication'.freeze
@@ -103,6 +105,8 @@ def browser
TOTP_SETUP_VISIT = 'TOTP Setup Visited'.freeze
TOTP_USER_DISABLED = 'TOTP: User Disabled TOTP'.freeze
TWILIO_PHONE_VALIDATION_FAILED = 'Twilio Phone Validation Failed'.freeze
+ TWILIO_SMS_INBOUND_MESSAGE_RECEIVED = 'Twilio SMS Inbound Message Received'.freeze
+ TWILIO_SMS_INBOUND_MESSAGE_VALIDATION_FAILED = 'Twilio SMS Inbound Validation Failed'.freeze
USER_REGISTRATION_AGENCY_HANDOFF_PAGE_VISIT = 'User registration: agency handoff visited'.freeze
USER_REGISTRATION_AGENCY_HANDOFF_COMPLETE = 'User registration: agency handoff complete'.freeze
USER_REGISTRATION_EMAIL = 'User Registration: Email Submitted'.freeze
diff --git a/app/services/otp_delivery_preference_updater.rb b/app/services/otp_delivery_preference_updater.rb
new file mode 100644
index 00000000000..2713dbbb2a2
--- /dev/null
+++ b/app/services/otp_delivery_preference_updater.rb
@@ -0,0 +1,29 @@
+class OtpDeliveryPreferenceUpdater
+ def initialize(user:, preference:, context:)
+ @user = user
+ @preference = preference
+ @context = context
+ end
+
+ def call
+ user_attributes = { otp_delivery_preference: preference }
+ UpdateUser.new(user: user, attributes: user_attributes).call if should_update_user?
+ end
+
+ private
+
+ attr_reader :user, :preference, :context
+
+ def should_update_user?
+ return false unless user
+ otp_delivery_preference_changed? && !idv_context?
+ end
+
+ def otp_delivery_preference_changed?
+ preference != user.otp_delivery_preference
+ end
+
+ def idv_context?
+ context == 'idv'
+ end
+end
diff --git a/app/services/phone_verification.rb b/app/services/phone_verification.rb
index 94abe87be27..1ecc430869a 100644
--- a/app/services/phone_verification.rb
+++ b/app/services/phone_verification.rb
@@ -18,9 +18,18 @@ def initialize(phone:, code:, locale: nil)
@locale = locale
end
+ # rubocop:disable Style/GuardClause
def send_sms
- raise VerifyError.new(code: error_code, message: error_message) unless start_request.success?
+ unless start_request.success?
+ raise VerifyError.new(
+ code: error_code,
+ message: error_message,
+ status: start_request.response_code,
+ response: start_request.response_body
+ )
+ end
end
+ # rubocop:enable Style/GuardClause
private
@@ -36,6 +45,8 @@ def error_message
def response_body
@response_body ||= JSON.parse(start_request.response_body)
+ rescue JSON::ParserError
+ {}
end
def start_request
@@ -73,11 +84,13 @@ def country_code
end
class VerifyError < StandardError
- attr_reader :code, :message
+ attr_reader :code, :message, :status, :response
- def initialize(code:, message:)
+ def initialize(code:, message:, status:, response:)
@code = code
@message = message
+ @status = status
+ @response = response
end
end
end
diff --git a/app/services/piv_cac_service.rb b/app/services/piv_cac_service.rb
index facd772de2d..6de250dc710 100644
--- a/app/services/piv_cac_service.rb
+++ b/app/services/piv_cac_service.rb
@@ -75,8 +75,6 @@ def decode_request(uri, token)
end
def authenticate(token)
- # TODO: make this secret required once we have everything deployed and configured
- # The piv/cac service side is pending, so this is not critical yet.
secret = Figaro.env.piv_cac_verify_token_secret
return '' if secret.blank?
nonce = SecureRandom.hex(10)
diff --git a/app/services/twilio_service.rb b/app/services/twilio_service.rb
index 53e4e52667c..fb866c21b61 100644
--- a/app/services/twilio_service.rb
+++ b/app/services/twilio_service.rb
@@ -1,71 +1,75 @@
require 'typhoeus/adapters/faraday'
-class TwilioService
- cattr_accessor :telephony_service do
- Twilio::REST::Client
- end
+module TwilioService
+ class Utils
+ cattr_accessor :telephony_service do
+ Twilio::REST::Client
+ end
- def initialize
- @client = if FeatureManagement.telephony_disabled?
- NullTwilioClient.new
- else
- twilio_client
- end
- @client.http_client.adapter = :typhoeus
- end
+ def initialize
+ @client = if FeatureManagement.telephony_disabled?
+ NullTwilioClient.new
+ else
+ twilio_client
+ end
+ @client.http_client.adapter = :typhoeus
+ end
- def place_call(params = {})
- sanitize_errors do
- params = params.reverse_merge(from: from_number)
- client.calls.create(params)
+ def place_call(params = {})
+ sanitize_errors do
+ params = params.reverse_merge(from: from_number)
+ client.calls.create(params)
+ end
end
- end
- def send_sms(params = {})
- sanitize_errors do
- params = params.reverse_merge(messaging_service_sid: Figaro.env.twilio_messaging_service_sid)
- client.messages.create(params)
+ def send_sms(params = {})
+ sanitize_errors do
+ params = params.reverse_merge(
+ messaging_service_sid: Figaro.env.twilio_messaging_service_sid
+ )
+ client.messages.create(params)
+ end
end
- end
- def phone_number
- @phone_number ||= random_phone_number
- end
+ def phone_number
+ @phone_number ||= random_phone_number
+ end
- def from_number
- "+1#{phone_number}"
- end
+ def from_number
+ "+1#{phone_number}"
+ end
- private
+ private
- attr_reader :client
+ attr_reader :client
- def twilio_client
- telephony_service.new(
- TWILIO_SID,
- TWILIO_AUTH_TOKEN
- )
- end
+ def twilio_client
+ telephony_service.new(
+ TWILIO_SID,
+ TWILIO_AUTH_TOKEN
+ )
+ end
- def random_phone_number
- TWILIO_NUMBERS.sample
- end
+ def random_phone_number
+ TWILIO_NUMBERS.sample
+ end
- def sanitize_errors
- yield
- rescue Twilio::REST::RestError => error
- sanitize_phone_number(error.message)
- raise
- end
+ def sanitize_errors
+ yield
+ rescue Twilio::REST::RestError => error
+ sanitize_phone_number(error.message)
+ raise
+ end
- DIGITS_TO_PRESERVE = 5
+ DIGITS_TO_PRESERVE = 5
- def sanitize_phone_number(str)
- str.gsub!(/\+[\d\(\)\- ]+/) do |match|
- digits_preserved = 0
+ def sanitize_phone_number(str)
+ str.gsub!(/\+[\d\(\)\- ]+/) do |match|
+ digits_preserved = 0
- match.gsub(/\d/) do |chr|
- (digits_preserved += 1) <= DIGITS_TO_PRESERVE ? chr : '#'
+ match.gsub(/\d/) do |chr|
+ (digits_preserved += 1) <= DIGITS_TO_PRESERVE ? chr : '#'
+ end
end
end
end
diff --git a/app/services/twilio_service/sms/request.rb b/app/services/twilio_service/sms/request.rb
new file mode 100644
index 00000000000..a02e2046c95
--- /dev/null
+++ b/app/services/twilio_service/sms/request.rb
@@ -0,0 +1,56 @@
+module TwilioService
+ module Sms
+ class Request
+ MESSAGE_PARAM_BODY = 'Body'.freeze
+ MESSAGE_PARAM_FROM = 'From'.freeze
+ MESSAGE_PARAM_FROM_COUNTRY = 'FromCountry'.freeze
+ MESSAGE_PARAM_MESSAGE_SID = 'MessageSid'.freeze # Twilio ID
+ SIGNATURE_HEADER = 'HTTP_X_TWILIO_SIGNATURE'.freeze
+
+ def initialize(url, params, signature)
+ @url = url
+ @params = params.reject { |key| key.downcase == key }
+ @signature = signature
+ end
+
+ def valid?
+ signature_valid? && message_valid?
+ end
+
+ def message
+ @message ||= params[Request::MESSAGE_PARAM_BODY.to_sym]&.downcase
+ end
+
+ def from
+ @from = params[Request::MESSAGE_PARAM_FROM.to_sym]
+ end
+
+ def extra_analytics_attributes
+ {
+ message_sid: params[Request::MESSAGE_PARAM_MESSAGE_SID.to_sym],
+ from_country: params[Request::MESSAGE_PARAM_FROM_COUNTRY.to_sym],
+ }
+ end
+
+ private
+
+ attr_reader :url, :params, :signature
+
+ # First, validate the message signature using Twilio's library:
+ # https://github.com/twilio/twilio-ruby/wiki/Request-Validator
+ def signature_valid?
+ Twilio::Security::RequestValidator.new(
+ Figaro.env.twilio_auth_token
+ ).validate(url, params, signature)
+ end
+
+ def message_valid?
+ message.present? && Response::MESSAGE_TYPES.include?(message)
+
+ # We may also want to validate the 'From' number against existing Users
+ # before processing the submission; however this is on-hold during the
+ # initial phase.
+ end
+ end
+ end
+end
diff --git a/app/services/twilio_service/sms/response.rb b/app/services/twilio_service/sms/response.rb
new file mode 100644
index 00000000000..f217e069ba3
--- /dev/null
+++ b/app/services/twilio_service/sms/response.rb
@@ -0,0 +1,39 @@
+module TwilioService
+ module Sms
+ class Response
+ MESSAGE_STOP_VARIANTS = %w[cancel end quit unsubscribe].freeze
+ MESSAGE_TYPES = %w[help stop].concat(MESSAGE_STOP_VARIANTS).freeze
+ SIGNATURE_HEADER = 'HTTP_X_TWILIO_SIGNATURE'.freeze
+
+ # CTIA short code guidelines require support for multiple stop words
+ MESSAGE_STOP_VARIANTS.each { |msg| define_method(msg) { send('stop') } }
+
+ delegate :extra_analytics_attributes, to: :request
+
+ def initialize(request)
+ @request = request
+ end
+
+ def reply
+ return unless request.valid?
+
+ {
+ to: request.from,
+ body: send(request.message),
+ }
+ end
+
+ private
+
+ attr_reader :request
+
+ def help
+ I18n.t('sms.help.message')
+ end
+
+ def stop
+ I18n.t('sms.stop.message')
+ end
+ end
+ end
+end
diff --git a/app/views/devise/sessions/new.html.slim b/app/views/devise/sessions/new.html.slim
index 36288c27330..cff0c8f1d01 100644
--- a/app/views/devise/sessions/new.html.slim
+++ b/app/views/devise/sessions/new.html.slim
@@ -11,7 +11,8 @@ h1.h3.my0 = decorated_session.new_session_heading
= f.input :email, label: t('account.index.email'), required: true, input_html: { class: 'mb4' }
= f.input :password, label: t('account.index.password'), required: true
= f.input :request_id, as: :hidden, input_html: { value: request_id }
- = f.button :submit, t('links.next'), class: 'btn-wide'
+ div
+ = f.button :submit, t('links.next'), class: 'sm-col-6 col-12 btn-wide mb3'
p.my3
= link_to t('notices.terms_of_service.link'), MarketingSite.privacy_url, target: '_blank'
diff --git a/app/views/exception_notifier/_session.text.erb b/app/views/exception_notifier/_session.text.erb
index 65583037bfc..496e1aa9abc 100644
--- a/app/views/exception_notifier/_session.text.erb
+++ b/app/views/exception_notifier/_session.text.erb
@@ -16,6 +16,8 @@ Referer: <%= @request.referer %>
<% session['user_return_to'] = session['user_return_to']&.split('?')&.first %>
Session: <%= session %>
-User UUID: <%= @kontroller.analytics_user.uuid %>
+<% user = @kontroller.analytics_user %>
+User UUID: <%= user.uuid %>
+User's Country (based on phone): <%= Phonelib.parse(user.phone).country %>
Visitor ID: <%= @request.cookies['ahoy_visitor'] %>
diff --git a/app/views/idv/jurisdiction/new.html.slim b/app/views/idv/jurisdiction/new.html.slim
index a03462cae6e..5a1303709ef 100644
--- a/app/views/idv/jurisdiction/new.html.slim
+++ b/app/views/idv/jurisdiction/new.html.slim
@@ -20,6 +20,7 @@ p.mt4.mb0 id='jurisdiction-label' = t('idv.messages.jurisdiction.where')
p = link_to t('idv.messages.jurisdiction.no_id'), idv_jurisdiction_fail_path(:no_id)
.mt4
- button type='submit' class='btn btn-primary btn-wide' = t('forms.buttons.continue')
+ button type='submit' class='btn btn-primary btn-wide sm-col-6 col-12'
+ = t('forms.buttons.continue')
= render 'shared/cancel', link: idv_cancel_path
diff --git a/app/views/idv/otp_delivery_method/new.html.slim b/app/views/idv/otp_delivery_method/new.html.slim
index b8f57fc798b..905bc44406c 100644
--- a/app/views/idv/otp_delivery_method/new.html.slim
+++ b/app/views/idv/otp_delivery_method/new.html.slim
@@ -1,33 +1,52 @@
h1.h3.my0 = t('idv.titles.otp_delivery_method')
-p = t('idv.messages.otp_delivery_method.phone_number_html',
+p.mt1 = t('idv.messages.otp_delivery_method.phone_number_html',
phone: @set_otp_delivery_method_presenter.phone)
= simple_form_for(@otp_delivery_selection_form, url: idv_otp_delivery_method_url,
- html: { autocomplete: 'off', method: 'put', role: 'form', class: 'mt2' }) do |f|
- fieldset.mb3.p0.border-none
- legend.mb1.h4.serif.bold = t('devise.two_factor_authentication.otp_delivery_preference.title')
- label.btn-border.col-12.sm-col-5.sm-mr2.mb2.sm-mb0
- .radio
- = radio_button_tag 'otp_delivery_selection_form[otp_delivery_preference]', :sms, true,
- class: :otp_delivery_preference_sms
- span.indicator
- = t('devise.two_factor_authentication.otp_delivery_preference.sms')
- - if @set_otp_delivery_method_presenter.sms_only?
- label.btn-border.col-12.sm-col-5.mb0.btn-disabled
+ html: { autocomplete: 'off', method: 'put', role: 'form', class: 'mt3' }) do |f|
+ fieldset.mb3.p0.border-none
+ label.btn-border.col-12.mb1
.radio
- = radio_button_tag 'otp_delivery_selection_form[otp_delivery_preference]', :voice, false,
- disabled: true,
- class: :otp_delivery_preference_voice
- span.indicator
- = t('devise.two_factor_authentication.otp_delivery_preference.voice')
- p.mt2.mb0 = @set_otp_delivery_method_presenter.phone_unsupported_message
- - else
- label.btn-border.col-12.sm-col-5.mb0
- .radio
- = radio_button_tag 'otp_delivery_selection_form[otp_delivery_preference]', :voice, false,
- class: :otp_delivery_preference_voice
- span.indicator
- = t('devise.two_factor_authentication.otp_delivery_preference.voice')
- = f.submit t('idv.buttons.send_confirmation_code'), type: :submit, class: 'btn btn-primary'
-.mt2.pt1.border-top
- = t('instructions.mfa.wrong_number_html',
- link: link_to(t('forms.two_factor.try_again'), idv_phone_path))
+ = radio_button_tag 'otp_delivery_selection_form[otp_delivery_preference]', :sms, true,
+ class: :otp_delivery_preference_sms
+ span.indicator.mt-tiny
+ span.blue.bold.fs-20p
+ = t('devise.two_factor_authentication.otp_delivery_preference.sms')
+ .regular.gray-dark.fs-10p.mb-tiny
+ = t('devise.two_factor_authentication.two_factor_choice_options.sms_info')
+ - if @set_otp_delivery_method_presenter.sms_only?
+ label.btn-border.col-12.mb0.btn-disabled
+ .radio
+ = radio_button_tag 'otp_delivery_selection_form[otp_delivery_preference]',
+ :voice, false,
+ disabled: true,
+ class: :otp_delivery_preference_voice
+ span.indicator.mt-tiny
+ span.blue.bold.fs-20p
+ = t('devise.two_factor_authentication.otp_delivery_preference.voice')
+ .regular.gray-dark.fs-10p.mb-tiny
+ = t('devise.two_factor_authentication.two_factor_choice_options.voice_info')
+ p.mt2.mb0 = @set_otp_delivery_method_presenter.phone_unsupported_message
+ - else
+ label.btn-border.col-12.mb0
+ .radio
+ = radio_button_tag 'otp_delivery_selection_form[otp_delivery_preference]',
+ :voice, false,
+ class: :otp_delivery_preference_voice
+ span.indicator.mt-tiny
+ span.blue.bold.fs-20p
+ = t('devise.two_factor_authentication.otp_delivery_preference.voice')
+ .regular.gray-dark.fs-10p.mb-tiny
+ = t('devise.two_factor_authentication.two_factor_choice_options.voice_info')
+ .mt3
+ = t('idv.form.no_alternate_phone_html',
+ link: link_to(t('idv.form.activate_by_mail'), idv_usps_path))
+ .mt2
+ = t('instructions.mfa.wrong_number_html',
+ link: link_to(t('forms.two_factor.try_again'), idv_phone_path))
+ .mt3
+ = f.submit t('idv.buttons.send_confirmation_code'),
+ type: :submit,
+ class: 'sm-col-6 col-12 btn btn-primary'
+.mt3.border-top
+ .mt1
+ = link_to t('links.cancel'), idv_phone_path
diff --git a/app/views/idv/phone/new.html.slim b/app/views/idv/phone/new.html.slim
index 95d85af95e8..034afb93361 100644
--- a/app/views/idv/phone/new.html.slim
+++ b/app/views/idv/phone/new.html.slim
@@ -18,7 +18,7 @@ ul.py1.m0
- if FeatureManagement.enable_usps_verification?
= render 'verification_options'
- = f.button :submit, t('forms.buttons.continue'), class: 'btn-wide mt6'
+ = f.button :submit, t('forms.buttons.continue'), class: 'btn-wide mt6 sm-col-6 col-12'
= render 'shared/cancel', link: idv_cancel_path
diff --git a/app/views/idv/sessions/new.html.slim b/app/views/idv/sessions/new.html.slim
index e5c465ee9be..7221333b143 100644
--- a/app/views/idv/sessions/new.html.slim
+++ b/app/views/idv/sessions/new.html.slim
@@ -72,6 +72,7 @@ p = link_to t('links.access_help'),
input_html: { class: 'zipcode', value: @view_model.idv_form.zipcode }
.mt3
- button type='submit' class='btn btn-primary btn-wide' = t('forms.buttons.continue')
+ button type='submit' class='btn btn-primary btn-wide sm-col-6 col-12'
+ = t('forms.buttons.continue')
= render 'shared/cancel', link: idv_cancel_path
= render @view_model.modal_partial, view_model: @view_model
diff --git a/app/views/idv/sessions/success.html.slim b/app/views/idv/sessions/success.html.slim
index 00c5607940c..1c29f42756d 100644
--- a/app/views/idv/sessions/success.html.slim
+++ b/app/views/idv/sessions/success.html.slim
@@ -10,6 +10,7 @@ h1.h3.mb2.mt3.my0 = t('idv.titles.session.success')
h2.h3.mb6.my0 = t('idv.messages.sessions.success')
-= link_to t('forms.buttons.continue'), idv_phone_path, class: 'btn btn-primary btn-wide'
+= link_to t('forms.buttons.continue'), idv_phone_path,
+ class: 'btn btn-primary btn-wide sm-col-6 col-12'
= render 'shared/cancel', link: idv_cancel_path
diff --git a/app/views/shared/_cancel_or_back_to_options.html.slim b/app/views/shared/_cancel_or_back_to_options.html.slim
index 82652316b46..2b00c63cb62 100644
--- a/app/views/shared/_cancel_or_back_to_options.html.slim
+++ b/app/views/shared/_cancel_or_back_to_options.html.slim
@@ -2,4 +2,4 @@
- if user_fully_authenticated?
= link_to cancel_link_text, account_path, class: 'h5'
- else
- = link_to t('devise.two_factor_authentication.two_factor_choice_cancel'), two_factor_options_path
+ = link_to t('two_factor_authentication.choose_another_option'), two_factor_options_path
diff --git a/app/views/shared/_fallback_links.html.slim b/app/views/shared/_fallback_links.html.slim
index 6538b7c9438..5ca036347c9 100644
--- a/app/views/shared/_fallback_links.html.slim
+++ b/app/views/shared/_fallback_links.html.slim
@@ -1,3 +1,6 @@
p.mt3#code-instructs = @presenter.help_text
-- presenter.fallback_links.each do |link|
- p = link
+h4.my0 = @presenter.fallback_question
+= link_to(t('two_factor_authentication.login_options_link_text'), \
+ login_two_factor_options_path)
+br
+br
diff --git a/app/views/shared/_personal_key.html.slim b/app/views/shared/_personal_key.html.slim
index 505a25d8f31..32b4d73434f 100644
--- a/app/views/shared/_personal_key.html.slim
+++ b/app/views/shared/_personal_key.html.slim
@@ -19,9 +19,10 @@ p.mt-tiny.mb0
= accordion('personal-key-info', t('users.personal_key.help_text_header')) do
= simple_format(t('users.personal_key.help_text'))
-= button_to t('forms.buttons.continue'), update_path,
- class: 'btn btn-primary btn-wide mb1 personal-key-continue',
- 'data-toggle': 'modal'
+div
+ = button_to t('forms.buttons.continue'), update_path,
+ class: 'btn btn-primary btn-wide mb1 personal-key-continue sm-col-6 col-12 mb3',
+ 'data-toggle': 'modal'
= render 'shared/personal_key_confirmation_modal', code: code, update_path: update_path
diff --git a/app/views/shared/_user_verify_password.html.slim b/app/views/shared/_user_verify_password.html.slim
index 6b67395e16c..bdffdb4d867 100644
--- a/app/views/shared/_user_verify_password.html.slim
+++ b/app/views/shared/_user_verify_password.html.slim
@@ -3,4 +3,4 @@ p = t('idv.messages.sessions.review_message')
= simple_form_for(current_user, url: update_path,
html: { autocomplete: 'off', method: :put, role: 'form' }) do |f|
= f.input :password, label: t('idv.form.password'), required: true
- = f.button :submit, t('forms.buttons.continue'), class: 'btn btn-primary btn-wide'
+ = f.button :submit, t('forms.buttons.continue'), class: 'btn btn-primary btn-wide sm-col-6 col-12'
diff --git a/app/views/sign_up/passwords/new.html.slim b/app/views/sign_up/passwords/new.html.slim
index 132231e19c4..a3ea86ba938 100644
--- a/app/views/sign_up/passwords/new.html.slim
+++ b/app/views/sign_up/passwords/new.html.slim
@@ -12,7 +12,8 @@ p.mt2.mb0#password-description
= render 'devise/shared/password_strength'
= hidden_field_tag :confirmation_token, @confirmation_token, id: 'confirmation_token'
= f.input :request_id, as: :hidden, input_html: { value: params[:request_id] || request_id }
- = f.button :submit, t('forms.buttons.continue'), class: 'btn-wide mb3'
+ div
+ = f.button :submit, t('forms.buttons.continue'), class: 'sm-col-6 col-12 btn-wide mb3'
= render 'shared/password_accordion'
diff --git a/app/views/two_factor_authentication/options/index.html.slim b/app/views/two_factor_authentication/options/index.html.slim
new file mode 100644
index 00000000000..75c6c1f3ed5
--- /dev/null
+++ b/app/views/two_factor_authentication/options/index.html.slim
@@ -0,0 +1,27 @@
+- title @presenter.title
+
+h1.h3.my0 = @presenter.heading
+p.mt-tiny.mb3 = @presenter.info
+
+= simple_form_for(@two_factor_options_form,
+ html: { autocomplete: 'off', role: 'form' },
+ method: :post,
+ url: login_two_factor_options_path) do |f|
+ .mb3
+ fieldset.m0.p0.border-none.
+ legend.mb1.h4.serif.bold = @presenter.label
+ - @presenter.options.each do |option|
+ label.btn-border.col-12.mb1 for="two_factor_options_form_selection_#{option.type}"
+ .radio
+ = radio_button_tag('two_factor_options_form[selection]',
+ option.type,
+ option.selected)
+ span.indicator.mt-tiny
+ span.blue.bold.fs-20p = option.label
+ .regular.gray-dark.fs-10p.mb-tiny = option.info
+
+ = f.button :submit, t('forms.buttons.continue')
+
+br
+p = @presenter.account_reset_or_cancel_link
+= render 'shared/cancel', link: destroy_user_session_path
diff --git a/app/views/two_factor_authentication/otp_verification/show.html.slim b/app/views/two_factor_authentication/otp_verification/show.html.slim
index cc206d8b409..48cb79737bb 100644
--- a/app/views/two_factor_authentication/otp_verification/show.html.slim
+++ b/app/views/two_factor_authentication/otp_verification/show.html.slim
@@ -14,13 +14,24 @@ p == @presenter.phone_number_message
autofocus: true, pattern: '[0-9]*', class: 'col-12 field monospace mfa',
'aria-describedby': 'code-instructs', maxlength: Devise.direct_otp_length,
autocomplete: 'off', type: 'tel')
- = submit_tag t('forms.buttons.submit.default'), class: 'btn btn-primary align-top'
+ = submit_tag t('forms.buttons.submit.default'), class: 'btn btn-primary align-top sm-col-6 col-12'
+ br
+ br
+ = link_to(t('links.two_factor_authentication.get_another_code'), \
+ otp_send_path(otp_delivery_selection_form: \
+ { otp_delivery_preference: @presenter.otp_delivery_preference, resend: true }),
+ class: 'btn btn-link btn-border ico ico-refresh text-decoration-none',
+ form_class: 'inline-block')
- if @presenter.remember_device_available?
- .border.col-9.rounded-lg.mt2
+ inline-block.span style="white-space:nowrap;"
= check_box_tag 'remember_device', true, false, class: 'my2 ml2 mr1'
= label_tag 'remember_device',
t('forms.messages.remember_device', duration: Figaro.env.remember_device_expiration_days),
class: 'blue'
-
-= render 'shared/fallback_links', presenter: @presenter
+br
+- if @presenter.update_phone_link.present?
+ br
+ = @presenter.update_phone_link
+- else
+ = render 'shared/fallback_links', presenter: @presenter
= render 'shared/cancel', link: @presenter.cancel_link
diff --git a/app/views/two_factor_authentication/personal_key_verification/show.html.slim b/app/views/two_factor_authentication/personal_key_verification/show.html.slim
index 00dacd73161..7c372eacd9d 100644
--- a/app/views/two_factor_authentication/personal_key_verification/show.html.slim
+++ b/app/views/two_factor_authentication/personal_key_verification/show.html.slim
@@ -8,4 +8,5 @@ p.mt-tiny.mb0 = t('devise.two_factor_authentication.personal_key_prompt')
= render 'partials/personal_key/entry_fields', f: f, attribute_name: :personal_key
= f.button :submit, t('forms.buttons.submit.default'), class: 'btn btn-primary'
+= render 'shared/fallback_links', presenter: @presenter
= render 'shared/cancel', link: sign_out_path
diff --git a/app/views/user_mailer/account_reset_cancel.html.slim b/app/views/user_mailer/account_reset_cancel.html.slim
new file mode 100644
index 00000000000..530ae2ec5f9
--- /dev/null
+++ b/app/views/user_mailer/account_reset_cancel.html.slim
@@ -0,0 +1,16 @@
+p.lead == t('.intro', app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'))
+
+table.spacer
+ tbody
+ tr
+ td.s10 height="10px"
+ |
+table.hr
+ tr
+ th
+ |
+
+p == t('.help',
+ app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'),
+ help_link: link_to(t('user_mailer.help_link_text'), MarketingSite.help_url),
+ contact_link: link_to(t('user_mailer.contact_link_text'), MarketingSite.contact_url))
diff --git a/app/views/users/phone_setup/index.html.slim b/app/views/users/phone_setup/index.html.slim
index 7728c1797f2..feeef635fc4 100644
--- a/app/views/users/phone_setup/index.html.slim
+++ b/app/views/users/phone_setup/index.html.slim
@@ -19,10 +19,11 @@ p.mt-tiny.mb0 = @presenter.info
strong.left = @presenter.label
= f.input :phone, as: :tel, label: false, required: true,
input_html: { class: 'phone col-8 mb4' }
- = f.button :submit, t('forms.buttons.send_security_code')
+ div
+ = f.button :submit, t('forms.buttons.send_security_code'), class: 'sm-col-6 col-12 btn-wide mb3'
.mt2.pt1.border-top
- path = current_user.piv_cac_enabled? ? account_recovery_setup_path : two_factor_options_path
- = link_to t('devise.two_factor_authentication.two_factor_choice_cancel'), path
+ = link_to t('two_factor_authentication.choose_another_option'), path
= stylesheet_link_tag 'intl-tel-number/intlTelInput'
= javascript_pack_tag 'intl-tel-input'
diff --git a/app/views/users/two_factor_authentication_setup/index.html.slim b/app/views/users/two_factor_authentication_setup/index.html.slim
index 85d136b722f..75ebf46d66f 100644
--- a/app/views/users/two_factor_authentication_setup/index.html.slim
+++ b/app/views/users/two_factor_authentication_setup/index.html.slim
@@ -20,6 +20,7 @@ p.mt-tiny.mb3 = @presenter.info
span.blue.bold.fs-20p = option.label
.regular.gray-dark.fs-10p.mb-tiny = option.info
- = f.button :submit, t('forms.buttons.continue')
+ div
+ = f.button :submit, t('forms.buttons.continue'), class: 'sm-col-6 col-12 btn-wide mb3'
= render 'shared/cancel', link: destroy_user_session_path
diff --git a/config/application.yml.example b/config/application.yml.example
index a4dcd267c5a..922451accb1 100644
--- a/config/application.yml.example
+++ b/config/application.yml.example
@@ -67,8 +67,6 @@ use_dashboard_service_providers: 'false'
dashboard_url: 'https://dashboard.demo.login.gov'
valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3"]'
-usps_mail_batch_hours: '24'
-
development:
aamva_cert_enabled: 'true'
aamva_public_key: '123abc'
@@ -82,7 +80,7 @@ development:
async_job_refresh_max_wait_seconds: '15'
attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5)
attribute_encryption_key: '2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25'
- attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111", "cost": "4000$8$4$" }, { "key": "key": "22222222222222222222222222222222", "cost": "4000$8$4$" }]'
+ attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111", "cost": "4000$8$4$" }, { "key": "22222222222222222222222222222222", "cost": "4000$8$4$" }]'
attribute_encryption_without_kms: 'false'
available_locales: 'en es fr'
aws_kms_key_id: 'alias/login-dot-gov-development-keymaker'
@@ -155,6 +153,7 @@ development:
piv_cac_service_url: 'https://localhost:8443/'
piv_cac_verify_token_url: 'https://localhost:8443/'
pkcs11_lib: '/opt/cloudhsm/lib/libcloudhsm_pkcs11.so'
+ programmable_sms_countries: 'US,CA,MX'
proofer_mock_fallback: 'true'
rack_mini_profiler: 'off'
reauthn_window: '120'
@@ -183,6 +182,8 @@ development:
twilio_numbers: '["9999999999","2222222222"]'
twilio_sid: 'sid1'
twilio_auth_token: 'token1'
+ twilio_http_basic_auth_username: '' # Twilio console configure Request URL
+ twilio_http_basic_auth_password: '' # https://www.twilio.com/docs/usage/security
twilio_messaging_service_sid: '123abc'
twilio_record_voice: 'true'
use_dashboard_service_providers: 'true'
@@ -199,10 +200,6 @@ development:
# These values serve as defaults for all production-like environments, which
# includes *.identitysandbox.gov and *.login.gov.
#
-# TODO: remove empty/fake values from this block, which create the misleading
-# impression that these values aren't used. In fact they will be used unless
-# they are overriden by keys with the same name in the application.yml in the
-# app secrets bucket.
production:
aamva_cert_enabled: 'true'
aamva_public_key: # Base64 encoded public key for AAMVA
@@ -276,9 +273,10 @@ production:
participate_in_dap: 'false' # pair with google_analytics_key
password_pepper: # generate via `rake secret`
password_strength_enabled: 'true'
- piv_cac_agencies: '["DOD"]'
+ piv_cac_agencies: '["DOD","NGA","EOP"]'
piv_cac_enabled: 'false'
pkcs11_lib: '/opt/cloudhsm/lib/libcloudhsm_pkcs11.so'
+ programmable_sms_countries: 'US,CA,MX'
proofer_mock_fallback: 'true'
reauthn_window: '120'
recaptcha_enabled_percent: '0'
@@ -304,6 +302,8 @@ production:
twilio_numbers: # Add JSON encoded array of phone numbers
twilio_sid: # Twilio SID
twilio_auth_token: # Twilio auth token
+ twilio_http_basic_auth_username: '' # Twilio console configure Request URL
+ twilio_http_basic_auth_password: '' # https://www.twilio.com/docs/usage/security
twilio_messaging_service_sid: # Twilio CoPilot SID
twilio_record_voice: 'false'
twilio_verify_api_key: 'change-me'
@@ -400,7 +400,9 @@ test:
piv_cac_verify_token_secret: '3ac13bfa23e22adae321194c083e783faf89469f6f85dcc0802b27475c94b5c3891b5657bd87d0c1ad65de459166440512f2311018db90d57b15d8ab6660748f'
piv_cac_verify_token_url: 'https://localhost:8443/'
pkcs11_lib: '/opt/cloudhsm/lib/libcloudhsm_pkcs11.so'
+ programmable_sms_countries: 'US,CA,MX'
proofer_mock_fallback: 'true'
+ programmable_sms_countries: 'US,CA'
reauthn_window: '120'
recaptcha_enabled_percent: '0'
recaptcha_site_key: 'key1'
@@ -425,6 +427,8 @@ test:
twilio_numbers: '["9999999999","2222222222"]'
twilio_sid: 'sid1'
twilio_auth_token: 'token1'
+ twilio_http_basic_auth_username: '' # Twilio console configure Request URL
+ twilio_http_basic_auth_password: '' # https://www.twilio.com/docs/usage/security
twilio_messaging_service_sid: '123abc'
twilio_record_voice: 'true'
twilio_verify_api_key: 'secret'
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 7a630bd1d5e..7f9b2d053ac 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -5,9 +5,9 @@
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
- config.action_controller.asset_host = Figaro.env.domain_name
config.action_controller.perform_caching = true
- config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ config.action_controller.asset_host = Figaro.env.asset_host || Figaro.env.domain_name
config.assets.js_compressor = :uglifier
config.assets.compile = false
config.assets.digest = true
@@ -19,7 +19,7 @@
host: Figaro.env.domain_name,
protocol: 'https',
}
- config.action_mailer.asset_host = Figaro.env.mailer_domain_name
+ config.action_mailer.asset_host = Figaro.env.asset_host || Figaro.env.mailer_domain_name
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_options = { from: Figaro.env.email_from }
config.action_mailer.delivery_method = if Figaro.env.disable_email_sending == 'true'
diff --git a/config/initializers/figaro.rb b/config/initializers/figaro.rb
index ed1d5602f6d..fef45c0cbf7 100644
--- a/config/initializers/figaro.rb
+++ b/config/initializers/figaro.rb
@@ -30,6 +30,7 @@
'password_max_attempts',
'password_pepper',
'password_strength_enabled',
+ 'programmable_sms_countries',
'queue_health_check_dead_interval_seconds',
'reauthn_window',
'recovery_code_length',
diff --git a/config/initializers/new_relic_tracers.rb b/config/initializers/new_relic_tracers.rb
index 7be3e33c66c..01fae6fa19b 100644
--- a/config/initializers/new_relic_tracers.rb
+++ b/config/initializers/new_relic_tracers.rb
@@ -66,7 +66,7 @@
add_method_tracer :encrypt, "Custom/#{name}/encrypt"
end
-TwilioService.class_eval do
+TwilioService::Utils.class_eval do
include ::NewRelic::Agent::MethodTracer
add_method_tracer :place_call, "Custom/#{name}/place_call"
add_method_tracer :send_sms, "Custom/#{name}/send_sms"
diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb
index 76c04b56f57..f78e27b0ecd 100644
--- a/config/initializers/secure_headers.rb
+++ b/config/initializers/secure_headers.rb
@@ -18,8 +18,8 @@
'*.nr-data.net',
'*.google-analytics.com',
],
- font_src: ["'self'", 'data:'],
- img_src: ["'self'", 'data:', 'login.gov'],
+ font_src: ["'self'", 'data:', Figaro.env.asset_host],
+ img_src: ["'self'", 'data:', 'login.gov', Figaro.env.asset_host],
media_src: ["'self'"],
object_src: ["'none'"],
script_src: [
@@ -30,8 +30,9 @@
'*.google-analytics.com',
'www.google.com',
'www.gstatic.com',
+ Figaro.env.asset_host,
],
- style_src: ["'self'"],
+ style_src: ["'self'", Figaro.env.asset_host],
base_uri: ["'self'"],
}
diff --git a/config/locales/account_reset/en.yml b/config/locales/account_reset/en.yml
index fe5a2b35f08..b165867510c 100644
--- a/config/locales/account_reset/en.yml
+++ b/config/locales/account_reset/en.yml
@@ -20,7 +20,7 @@ en:
and you will need to restore each connection.
If you continue, you
will first receive an email confirmation. As a security measure, you will
receive another email with the link to continue deleting your account 24 hours
- after the intial confirmation email arrives.
+ after the initial confirmation email arrives.
are_you_sure: Are you sure you don't have access to any of your security methods
OR your personal key?
no_cancel: No, cancel
diff --git a/config/locales/devise/en.yml b/config/locales/devise/en.yml
index e9602f0374c..33f2bbc98c2 100644
--- a/config/locales/devise/en.yml
+++ b/config/locales/devise/en.yml
@@ -146,7 +146,7 @@ en:
code below.
please_try_again_html: Please try again in %{time_remaining}.
account_reset:
- successful_cancel: Thank you. The request to delete your login.gov account
+ successful_cancel: Thank you. Your request to delete your login.gov account
has been cancelled.
link: deleting your account
text_html: If you can't use any of these security options above, you can reset
@@ -178,23 +178,6 @@ en:
piv_cac_info: Use your PIV/CAC card to secure your account.
two_factor_choice_intro: login.gov makes sure you can access your account by
adding a second layer of security.
- two_factor_choice_cancel: "‹ Choose another option"
- two_factor_login_choice: Secure your account
- two_factor_login_choice_options:
- voice: Phone call
- voice_info: Get your security code via phone call.
- sms: Text message / SMS
- sms_info: Get your security code via text message / SMS.
- auth_app: Authentication application
- auth_app_info: Set up an authentication application to get your security code
- without providing a phone number.
- piv_cac: Government employees
- piv_cac_info: Use your PIV/CAC card to secure your account.
- personal_key: Personal Key
- personal_key_info: Use the 12 character personal key you used at account creation.
- two_factor_login_choice_intro: login.gov makes sure you can access your account
- by adding a second layer of security.
- two_factor_login_choice_cancel: "‹ Choose another option"
two_factor_setup: Add a phone number
user:
new_otp_sent: We sent you a new one-time security code.
diff --git a/config/locales/devise/es.yml b/config/locales/devise/es.yml
index 1e273df55ee..242c09a1ac1 100644
--- a/config/locales/devise/es.yml
+++ b/config/locales/devise/es.yml
@@ -151,8 +151,8 @@ es:
el código de seguridad a continuación.
please_try_again_html: Inténtelo de nuevo en %{time_remaining}.
account_reset:
- successful_cancel: Gracias. La solicitud para eliminar su cuenta de login.gov
- ha sido cancelado.
+ successful_cancel: Gracias. Su solicitud para eliminar su cuenta de login.gov
+ ha sido cancelada.
link: eliminando su cuenta
text_html: Si no puede usar ninguna de estas opciones de seguridad anteriores,
puede restablecer tus preferencias por %{link}.
@@ -184,24 +184,6 @@ es:
piv_cac_info: Use su tarjeta PIV / CAC para asegurar su cuenta.
two_factor_choice_intro: login.gov se asegura de que pueda acceder a su cuenta
agregando una segunda capa de seguridad.
- two_factor_choice_cancel: "‹ Elige otra opción"
- two_factor_login_choice: Asegure su cuenta
- two_factor_login_choice_options:
- voice: Llamada telefónica
- voice_info: Obtenga su código de seguridad a través de una llamada telefónica.
- sms: Mensaje de texto / SMS
- sms_info: Obtenga su código de seguridad a través de mensajes de texto / SMS.
- auth_app: Aplicación de autenticación
- auth_app_info: Configure una aplicación de autenticación para obtener su código
- de seguridad sin proporcionar un número de teléfono.
- piv_cac: Empleados del Gobierno
- piv_cac_info: Use su tarjeta PIV / CAC para asegurar su cuenta.
- personal_key: Clave personal
- personal_key_info: Use la clave personal de 12 caracteres que usó en la creación
- de la cuenta.
- two_factor_login_choice_intro: login.gov se asegura de que pueda acceder a su
- cuenta agregando una segunda capa de seguridad.
- two_factor_login_choice_cancel: "‹ Elige otra opción"
two_factor_setup: Añada un número de teléfono
user:
new_otp_sent: Le enviamos un nuevo código de sólo un uso
diff --git a/config/locales/devise/fr.yml b/config/locales/devise/fr.yml
index f9f764116c5..a9f2ea7d1f7 100644
--- a/config/locales/devise/fr.yml
+++ b/config/locales/devise/fr.yml
@@ -158,8 +158,8 @@ fr:
le code de sécurité ci-dessous.
please_try_again_html: Veuillez essayer de nouveau dans %{time_remaining}.
account_reset:
- successful_cancel: Je vous remercie. La demande de suppression de votre compte
- login.gov a été annulé.
+ successful_cancel: Je vous remercie. Votre demande de suppression de votre
+ compte login.gov a été annulée.
link: supprimer votre compte
text_html: Si vous ne pouvez pas utiliser l'une de ces options de sécurité
ci-dessus, vous pouvez réinitialiser vos préférences par %{link}.
@@ -193,24 +193,6 @@ fr:
piv_cac_info: Utilisez votre carte PIV / CAC pour sécuriser votre compte.
two_factor_choice_intro: login.gov s'assure que vous pouvez accéder à votre
compte en ajoutant une deuxième couche de sécurité.
- two_factor_choice_cancel: "‹ Choisissez une autre option"
- two_factor_login_choice: Sécurise ton compte
- two_factor_login_choice_options:
- voice: Appel téléphonique
- voice_info: Obtenez votre code de sécurité par appel téléphonique.
- sms: SMS
- sms_info: Obtenez votre code de sécurité par SMS.
- auth_app: Application d'authentification
- auth_app_info: Configurez une application d'authentification pour obtenir
- votre code de sécurité sans fournir de numéro de téléphone.
- piv_cac: Employés du gouvernement
- piv_cac_info: Utilisez votre carte PIV / CAC pour sécuriser votre compte.
- personal_key: Clé personnelle
- personal_key_info: Utilisez la clé personnelle de 12 caractères que vous avez
- utilisée lors de la création du compte.
- two_factor_login_choice_intro: login.gov s'assure que vous pouvez accéder Ã
- votre compte en ajoutant une deuxième couche de sécurité.
- two_factor_login_choice_cancel: "‹ Choisissez une autre option"
two_factor_setup: Ajoutez un numéro de téléphone
user:
new_otp_sent: Nous vous avons envoyé un code de sécurité à utilisation unique.
diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml
index d611f1aa46f..5dfe8ea8114 100644
--- a/config/locales/errors/en.yml
+++ b/config/locales/errors/en.yml
@@ -40,7 +40,10 @@ en:
otp_incorrect: Incorrect code. Did you type it correctly?
password_incorrect: Incorrect password
personal_key_incorrect: Incorrect personal key
+ phone_unsupported: Sorry, we are unable to send SMS at this time. Please try
+ the phone call option below, or use your personal key.
requires_phone: requires you to enter your phone number.
+ twilio_inbound_sms_invalid: The inbound Twilio SMS message failed validation.
unauthorized_authn_context: Unauthorized authentication context
unauthorized_nameid_format: Unauthorized nameID format
unauthorized_service_provider: Unauthorized Service Provider
diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml
index dc35a569c1b..3f5cd91d7ed 100644
--- a/config/locales/errors/es.yml
+++ b/config/locales/errors/es.yml
@@ -38,7 +38,10 @@ es:
otp_incorrect: El código es incorrecto. ¿Lo escribió correctamente?
password_incorrect: La contraseña es incorrecta
personal_key_incorrect: La clave personal es incorrecta
+ phone_unsupported: Lo sentimos, no podemos enviar SMS en este momento. Pruebe
+ la opción de llamada telefónica a continuación o use su clave personal.
requires_phone: requiere que ingrese su número de teléfono.
+ twilio_inbound_sms_invalid: El mensaje de Twilio SMS de entrada falló la validación.
unauthorized_authn_context: Contexto de autenticación no autorizado
unauthorized_nameid_format: NOT TRANSLATED YET
unauthorized_service_provider: Proveedor de servicio no autorizado
diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml
index edb0d0a551c..7b14eebc229 100644
--- a/config/locales/errors/fr.yml
+++ b/config/locales/errors/fr.yml
@@ -7,7 +7,7 @@ fr:
invalid_totp: Code non valide. Veuillez essayer de nouveau.
max_password_attempts_reached: Vous avez inscrit des mots de passe incorrects
un trop grand nombre de fois. Vous pouvez réinitialiser votre mot de passe en
- utilisant le lien « Vous avez oublié votre mot de passe? ».
+ utilisant le lien « Vous avez oublié votre mot de passe? ».
messages:
already_confirmed: a déjà été confirmé, veuillez essayer de vous connecter
blank: Veuillez remplir ce champ.
@@ -15,7 +15,7 @@ fr:
confirmation_invalid_token: Lien de confirmation non valide. Le lien est expiré
ou vous avez déjà confirmé votre compte.
confirmation_period_expired: Lien de confirmation expiré. Vous pouvez cliquer
- sur « Envoyer les instructions de confirmation de nouveau » pour en obtenir
+ sur « Envoyer les instructions de confirmation de nouveau » pour en obtenir
un autre.
expired: est expiré, veuillez en demander un nouveau
format_mismatch: Veuillez vous assurer de respecter le format requis.
@@ -33,13 +33,18 @@ fr:
not_found: introuvable
not_locked: n'a pas été verrouillé
not_saved:
- one: '1Â erreur a interdit la sauvegarde de cette %{resource}Â :'
- other: "%{count} des erreurs ont empêché la sauvegarde de cette %{resource} :"
+ one: '1 erreur a interdit la sauvegarde de cette %{resource} :'
+ other: "%{count} des erreurs ont empêché la sauvegarde de cette %{resource}
+ :"
otp_failed: NOT TRANSLATED YET
otp_incorrect: Code non valide. L'avez-vous inscrit correctement?
password_incorrect: Mot de passe incorrect
personal_key_incorrect: Clé personnelle incorrecte
+ phone_unsupported: Désolé, nous ne sommes pas en mesure d'envoyer des SMS pour
+ le moment. S'il vous plaît essayez l'option d'appel téléphonique ci-dessous,
+ ou utilisez votre clé personnelle.
requires_phone: vous demande d'entrer votre numéro de téléphone.
+ twilio_inbound_sms_invalid: Le message SMS Twilio entrant a échoué à la validation.
unauthorized_authn_context: Contexte d'authentification non autorisé
unauthorized_nameid_format: NOT TRANSLATED YET
unauthorized_service_provider: Fournisseur de service non autorisé
diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml
index 95e23031361..52e4a432f3e 100644
--- a/config/locales/idv/en.yml
+++ b/config/locales/idv/en.yml
@@ -8,7 +8,7 @@ en:
mail:
resend: Send another letter
send: Send a letter
- send_confirmation_code: Send confirmation code
+ send_confirmation_code: Continue
cancel:
modal_header: Are you sure you want to cancel?
warning_header: If you cancel now
@@ -96,7 +96,7 @@ en:
loading: Verifying your identity
mail_sent: Your letter is on its way
otp_delivery_method:
- phone_number_html: We will send a code to %{phone}
+ phone_number_html: We'll send a code to %{phone}
personal_key: This is your new personal key. Write it down and keep it in a
safe place. You will need it if you ever lose your password.
phone:
@@ -177,7 +177,7 @@ en:
resend: Want another letter?
verify: Want a letter?
no_id: We are unable to verify your identity without a state-issued ID
- otp_delivery_method: Get a code by phone
+ otp_delivery_method: How would you like to receive a code?
phone: Phone number of record
review: Review and submit
select_verification: Choose how to confirm your address
diff --git a/config/locales/jobs/en.yml b/config/locales/jobs/en.yml
index d7d47937775..e2b078abf16 100644
--- a/config/locales/jobs/en.yml
+++ b/config/locales/jobs/en.yml
@@ -4,6 +4,8 @@ en:
sms_otp_sender_job:
message: "%{code} is your %{app} one-time security code. This code will expire
in %{expiration} minutes."
+ sms_account_reset_cancel_job:
+ message: Your request to delete your login.gov account has been cancelled.
sms_account_reset_notifier_job:
message: 'You''ve requested to delete your login.gov account. Your request will
be processed in 24 hours. If you don’t want to delete your account, please
diff --git a/config/locales/jobs/es.yml b/config/locales/jobs/es.yml
index e0f5c767b71..6ead589ceff 100644
--- a/config/locales/jobs/es.yml
+++ b/config/locales/jobs/es.yml
@@ -4,6 +4,8 @@ es:
sms_otp_sender_job:
message: "%{code} es su %{app} código de seguridad de sólo un uso. Este código
caducará en %{expiration} minutos."
+ sms_account_reset_cancel_job:
+ message: Su solicitud para eliminar su cuenta de login.gov ha sido cancelada.
sms_account_reset_notifier_job:
message: 'Has solicitado eliminar tu cuenta de login.gov. Su solicitud será
ser procesado en 24 horas. Si no desea eliminar su cuenta, por favor cancelar:
diff --git a/config/locales/jobs/fr.yml b/config/locales/jobs/fr.yml
index 76c1a828e42..f4509827b37 100644
--- a/config/locales/jobs/fr.yml
+++ b/config/locales/jobs/fr.yml
@@ -4,6 +4,8 @@ fr:
sms_otp_sender_job:
message: "%{code} est votre %{app} code de sécurité à utilisation unique. Ce
code expirera dans %{expiration} minutes."
+ sms_account_reset_cancel_job:
+ message: Votre demande de suppression de votre compte login.gov a été annulée.
sms_account_reset_notifier_job:
message: 'Vous avez demandé à supprimer votre compte login.gov. Votre demande
sera être traité en 24 heures. Si vous ne souhaitez pas supprimer votre compte,
diff --git a/config/locales/links/en.yml b/config/locales/links/en.yml
index 2013caa010f..530e926d66b 100644
--- a/config/locales/links/en.yml
+++ b/config/locales/links/en.yml
@@ -17,7 +17,6 @@ en:
passwords:
forgot: Forgot your password?
phone_confirmation:
- auth_app_fallback_html: " or %{link}."
fallback_to_sms_html: Send me a text message with the code instead
fallback_to_voice_html: If you can't get text message right now, you can get
a security code via %{link}
@@ -29,10 +28,5 @@ en:
sign_out: Sign out
two_factor_authentication:
app_option: Use an authentication application instead.
- app: get a security code via authentication app
- resend_code:
- sms: Get another text message
- voice: Get another phone call
- sms: get a security code via text message
- voice: get a security code via phone call
+ get_another_code: Get another code
what_is_totp: What is an authentication app?
diff --git a/config/locales/links/es.yml b/config/locales/links/es.yml
index 85038930325..d73a441cba8 100644
--- a/config/locales/links/es.yml
+++ b/config/locales/links/es.yml
@@ -17,7 +17,6 @@ es:
passwords:
forgot: "¿Olvidó su contraseña?"
phone_confirmation:
- auth_app_fallback_html: o %{link}.
fallback_to_sms_html: EnvÃeme un mensaje de texto con el código en su lugar
fallback_to_voice_html: Si no puede recibir un mensaje de texto ahora mismo,
puede obtener un código de seguridad a través de %{link}
@@ -29,10 +28,5 @@ es:
sign_out: Cerrar sesión
two_factor_authentication:
app_option: Use una aplicación de autenticación en su lugar.
- app: Obtener un código de seguridad mediante la app de autenticación
- resend_code:
- sms: Obtener otro mensaje de texto
- voice: Obtener otra llamada telefónica
- sms: Obtener un código de seguridad a través de un mensaje de texto
- voice: Obtener un código de seguridad a través de una llamada telefónica
+ get_another_code: Obtener otro código
what_is_totp: "¿Qué es una app de autenticación?"
diff --git a/config/locales/links/fr.yml b/config/locales/links/fr.yml
index 7374d160fcf..2005e358dea 100644
--- a/config/locales/links/fr.yml
+++ b/config/locales/links/fr.yml
@@ -17,7 +17,6 @@ fr:
passwords:
forgot: Vous avez oublié votre mot de passe?
phone_confirmation:
- auth_app_fallback_html: " ou %{link}."
fallback_to_sms_html: Envoyez-moi plutôt un SMS contenant le code
fallback_to_voice_html: Si vous ne pouvez recevoir de message texte pour le
moment, vous pouvez obtenir un code de sécurité par %{link}
@@ -29,10 +28,5 @@ fr:
sign_out: Déconnexion
two_factor_authentication:
app_option: Utilisez une application d'authentification à la place.
- app: obtenir un code de sécurité par l'application d'authentification
- resend_code:
- sms: Recevoir un autre SMS
- voice: Recevoir un autre appel téléphonique
- sms: obtenir un code de sécurité par SMS
- voice: obtenir un code par un appel téléphonique
+ get_another_code: Obtenir un autre code
what_is_totp: Qu'est-ce qu'une application d'authentification?
diff --git a/config/locales/sms/en.yml b/config/locales/sms/en.yml
new file mode 100644
index 00000000000..8997dd2f8c1
--- /dev/null
+++ b/config/locales/sms/en.yml
@@ -0,0 +1,8 @@
+---
+en:
+ sms:
+ help:
+ message: 'login.gov: Get help at www.login.gov/help or email us at hello@login.gov.'
+ stop:
+ message: login.gov is not a subscription service. You will only receive security
+ codes when you sign in. Reply HELP for help or see www.login.gov/help.
diff --git a/config/locales/sms/es.yml b/config/locales/sms/es.yml
new file mode 100644
index 00000000000..911a91dcf10
--- /dev/null
+++ b/config/locales/sms/es.yml
@@ -0,0 +1,9 @@
+---
+es:
+ sms:
+ help:
+ message: 'login.gov: obtenga ayuda en www.login.gov/help o envÃenos un correo
+ electrónico a hello@login.gov.'
+ stop:
+ message: login.gov no es un servicio de suscripción. Solo recibirá códigos de
+ seguridad cuando se registre. Responda HELP para obtener ayuda o visite www.login.gov/help.
diff --git a/config/locales/sms/fr.yml b/config/locales/sms/fr.yml
new file mode 100644
index 00000000000..7bf85d11c8f
--- /dev/null
+++ b/config/locales/sms/fr.yml
@@ -0,0 +1,10 @@
+---
+fr:
+ sms:
+ help:
+ message: 'login.gov: Obtenez de l''aide sur www.login.gov/help ou envoyez-nous
+ un courriel à hello@login.gov.'
+ stop:
+ message: login.gov n'est pas un service d'abonnement. Vous ne recevrez des codes
+ de sécurité que lorsque vous vous connecterez. Aidez HELP pour obtenir de
+ l'aide ou consultez www.login.gov/help.
diff --git a/config/locales/two_factor_authentication/en.yml b/config/locales/two_factor_authentication/en.yml
new file mode 100644
index 00000000000..39db0d54380
--- /dev/null
+++ b/config/locales/two_factor_authentication/en.yml
@@ -0,0 +1,27 @@
+---
+en:
+ two_factor_authentication:
+ login_options:
+ auth_app: Authentication app
+ auth_app_info: Use your authentication application to get security code.
+ personal_key: Personal Key
+ personal_key_info: Use the 16 character personal key you received at account
+ creation.
+ piv_cac: Government employee ID
+ piv_cac_info: Use your PIV/CAC card instead of a security code.
+ sms: Text message
+ sms_info: Get security code via text message.
+ voice: Automated phone call
+ voice_info: Get security code via phone call (North America phone numbers only).
+ login_options_title: Select your security option
+ login_options_link_text: Choose another security option
+ login_intro: You set these up when you created your account
+ choose_another_option: "‹ Choose another option"
+ personal_key_fallback:
+ question: Don't have your personal key?
+ phone_fallback:
+ question: Don't have access to your phone right now?
+ piv_cac_fallback:
+ question: Don't have your piv/cac card available?
+ totp_fallback:
+ question: Don't have your authenticator app?
diff --git a/config/locales/two_factor_authentication/es.yml b/config/locales/two_factor_authentication/es.yml
new file mode 100644
index 00000000000..021b5307714
--- /dev/null
+++ b/config/locales/two_factor_authentication/es.yml
@@ -0,0 +1,29 @@
+---
+es:
+ two_factor_authentication:
+ login_options:
+ auth_app: Aplicación de autenticación
+ auth_app_info: Use su aplicación de autenticación para obtener el código de
+ seguridad.
+ personal_key: Clave personal
+ personal_key_info: Use la clave personal de 16 caracteres que usó en la creación
+ de la cuenta.
+ piv_cac: Empleados del Gobierno
+ piv_cac_info: Use su tarjeta PIV / CAC para asegurar su cuenta.
+ sms: Mensaje de texto / SMS
+ sms_info: Obtenga su código de seguridad a través de mensajes de texto / SMS.
+ voice: Llamada telefónica automatizada
+ voice_info: Obtenga su código de seguridad a través de una llamada telefónica.
+ (Solo números de teléfono de América del Norte).
+ login_options_title: Seleccione su opción de seguridad
+ login_options_link_text: Elige otra opción de seguridad
+ login_intro: Usted configuró esto cuando creó su cuenta
+ choose_another_option: "‹ Elige otra opción"
+ personal_key_fallback:
+ question: "¿No tiene su clave personal?"
+ phone_fallback:
+ question: "¿No tiene acceso a su teléfono ahora mismo?"
+ piv_cac_fallback:
+ question: "¿No tiene su tarjeta PIV/CAC disponible?"
+ totp_fallback:
+ question: "¿No tiene su aplicación de autenticación?"
diff --git a/config/locales/two_factor_authentication/fr.yml b/config/locales/two_factor_authentication/fr.yml
new file mode 100644
index 00000000000..43ac3727793
--- /dev/null
+++ b/config/locales/two_factor_authentication/fr.yml
@@ -0,0 +1,29 @@
+---
+fr:
+ two_factor_authentication:
+ login_options:
+ auth_app: Application d'authentification
+ auth_app_info: Utilisez votre application d'authentification pour obtenir votre
+ code de sécurité
+ personal_key: Clé personnelle
+ personal_key_info: Utilisez la clé personnelle de 16 caractères que vous avez
+ utilisée lors de la création du compte.
+ piv_cac: Employés du gouvernement
+ piv_cac_info: Utilisez votre carte PIV / CAC pour sécuriser votre compte.
+ sms: SMS
+ sms_info: Obtenez votre code de sécurité par SMS.
+ voice: Appel téléphonique
+ voice_info: Obtenez votre code de sécurité par appel téléphonique. (Seulement
+ les numéros de téléphone en Amerique du Nord)
+ login_options_title: Sélectionnez votre option de sécurité
+ login_options_link_text: Choisissez une autre option de sécurité
+ login_intro: Vous les avez configurés lorsque vous avez crée votre compte
+ choose_another_option: "‹ Choisissez une autre option"
+ personal_key_fallback:
+ question: Vous n'avez pas votre clé personnelle?
+ phone_fallback:
+ question: Vous n'avez pas accès à votre téléphone maintenant?
+ piv_cac_fallback:
+ question: Vous n'avez pas accès à votre carte PIV/CAC?
+ totp_fallback:
+ question: Vous n'avez pas votre application d'authentification?
diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml
index 0ba57faca25..21e09a1bfdf 100644
--- a/config/locales/user_mailer/en.yml
+++ b/config/locales/user_mailer/en.yml
@@ -8,6 +8,11 @@ en:
didn't request a password reset, you can ignore this message.
link_text: Create your account
subject: Your login.gov password reset request
+ account_reset_cancel:
+ intro: This email confirms you have cancelled the request to delete your login.gov
+ account.
+ subject: Delete your account
+ help: ''
account_reset_request:
help: ''
intro: You’ve requested to delete your login.gov account.
Your
diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml
index 0c4d9b723a7..a7349619979 100644
--- a/config/locales/user_mailer/es.yml
+++ b/config/locales/user_mailer/es.yml
@@ -5,6 +5,11 @@ es:
intro: NOT TRANSLATED YET
link_text: NOT TRANSLATED YET
subject: NOT TRANSLATED YET
+ account_reset_cancel:
+ intro: Este correo electrónico confirma que ha cancelado la solicitud para eliminar
+ su cuenta de login.gov.
+ subject: Eliminar su cuenta
+ help: ''
account_reset_request:
help: ''
intro: Has solicitado eliminar tu cuenta de login.gov.
Su la
diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml
index 632f237141b..18556d9a1fd 100644
--- a/config/locales/user_mailer/fr.yml
+++ b/config/locales/user_mailer/fr.yml
@@ -5,6 +5,11 @@ fr:
intro: NOT TRANSLATED YET
link_text: NOT TRANSLATED YET
subject: NOT TRANSLATED YET
+ account_reset_cancel:
+ intro: Cet e-mail confirme que vous avez annulé la demande de suppression de
+ votre compte login.gov.
+ subject: Supprimer votre compte
+ help: ''
account_reset_request:
help: ''
intro: Vous avez demandé à supprimer votre compte login.gov.
Votre
diff --git a/config/routes.rb b/config/routes.rb
index 94d589e86c9..e48f79a500e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -26,6 +26,9 @@
match "/api/saml/auth#{suffix}" => 'saml_idp#auth', via: %i[get post]
end
+ # Twilio Request URL for inbound SMS
+ post '/api/sms/receive' => 'sms#receive'
+
post '/api/service_provider' => 'service_provider#update'
match '/api/voice/otp' => 'voice/otp#show',
via: %i[get post],
@@ -63,6 +66,9 @@
get '/account_reset/confirm_delete_account' => 'account_reset/confirm_delete_account#show'
post '/api/account_reset/send_notifications' => 'account_reset/send_notifications#update'
+ get '/login/two_factor/options' => 'two_factor_authentication/options#index'
+ post '/login/two_factor/options' => 'two_factor_authentication/options#create'
+
get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show'
post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create'
get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show'
diff --git a/config/service_providers.yml b/config/service_providers.yml
index d34bef38a94..815c235aea9 100644
--- a/config/service_providers.yml
+++ b/config/service_providers.yml
@@ -582,6 +582,7 @@ production:
- 'https://portal.dot.gov/openid-connect/dot_login'
- 'https://fmcsa.portal.dot.gov/openid-connect/dot_login'
- 'https://portal.dot.gov/'
+ - 'https://score.fmcsa.dot.gov/'
restrict_to_deploy_env: 'prod'
# NGA GEOWorks Symphony
@@ -717,3 +718,18 @@ production:
redirect_uris:
- 'gov.dhs.cbp.opa.mycbp://result'
restrict_to_deploy_env: 'prod'
+
+ # A DoD system that allows tsps to manage moves.
+ 'urn:gov:gsa:openidconnect.profiles:sp:sso:dod:tspmovemilprod':
+ agency_id: 8
+ friendly_name: 'tsp.move.mil'
+ agency: 'DOD'
+ logo: 'move_mil.svg'
+ cert: 'move_mil_prod'
+ return_to_sp_url: 'https://tsp.move.mil'
+ redirect_uris:
+ - 'https://tsp.move.mil'
+ - 'https://tsp.move.mil/auth/login-gov/callback'
+ attribute_bundle:
+ - email
+ restrict_to_deploy_env: 'prod'
diff --git a/db/migrate/20180709141748_drop_encrypted_password_column_from_user.rb b/db/migrate/20180709141748_drop_encrypted_password_column_from_user.rb
new file mode 100644
index 00000000000..d6fce556d29
--- /dev/null
+++ b/db/migrate/20180709141748_drop_encrypted_password_column_from_user.rb
@@ -0,0 +1,15 @@
+class DropEncryptedPasswordColumnFromUser < ActiveRecord::Migration[5.1]
+ def up
+ safety_assured do
+ remove_column :users, :encrypted_password
+ remove_column :users, :password_salt
+ remove_column :users, :password_cost
+ end
+ end
+
+ def down
+ add_column :users, :encrypted_password, :string, limit: 255, default: ''
+ add_column :users, :password_salt, :string
+ add_column :users, :password_cost, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b5d46bfb506..8ab5f52df9a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180620233914) do
+ActiveRecord::Schema.define(version: 20180709141748) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -171,7 +171,6 @@
end
create_table "users", force: :cascade do |t|
- t.string "encrypted_password", limit: 255, default: ""
t.string "reset_password_token", limit: 255
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
@@ -201,11 +200,9 @@
t.datetime "idv_attempted_at"
t.integer "idv_attempts", default: 0
t.string "recovery_code"
- t.string "password_salt"
t.string "encryption_key"
t.string "unique_session_id"
t.string "recovery_salt"
- t.string "password_cost"
t.string "recovery_cost"
t.string "email_fingerprint", default: "", null: false
t.text "encrypted_email", default: "", null: false
diff --git a/lib/proofer_mocks/address_mock.rb b/lib/proofer_mocks/address_mock.rb
index 2a4983c84c8..530ac83d129 100644
--- a/lib/proofer_mocks/address_mock.rb
+++ b/lib/proofer_mocks/address_mock.rb
@@ -1,5 +1,5 @@
class AddressMock < Proofer::Base
- attributes :phone
+ required_attributes :phone
stage :address
diff --git a/lib/proofer_mocks/resolution_mock.rb b/lib/proofer_mocks/resolution_mock.rb
index 5e256e149b1..4fc373e39f0 100644
--- a/lib/proofer_mocks/resolution_mock.rb
+++ b/lib/proofer_mocks/resolution_mock.rb
@@ -1,5 +1,5 @@
class ResolutionMock < Proofer::Base
- attributes :first_name, :ssn, :zipcode
+ required_attributes :first_name, :ssn, :zipcode
stage :resolution
diff --git a/lib/proofer_mocks/state_id_mock.rb b/lib/proofer_mocks/state_id_mock.rb
index 0ea04dc31a5..764de54ab75 100644
--- a/lib/proofer_mocks/state_id_mock.rb
+++ b/lib/proofer_mocks/state_id_mock.rb
@@ -7,7 +7,7 @@ class StateIdMock < Proofer::Base
drivers_license drivers_permit state_id_card
].freeze
- attributes :state_id_number, :state_id_type, :state_id_jurisdiction
+ required_attributes :state_id_number, :state_id_type, :state_id_jurisdiction
stage :state_id
diff --git a/lib/tasks/rotate.rake b/lib/tasks/rotate.rake
index 6a599ef7523..ac6d9657b8c 100644
--- a/lib/tasks/rotate.rake
+++ b/lib/tasks/rotate.rake
@@ -11,9 +11,13 @@ namespace :rotate do
User.find_in_batches.with_index do |users, _batch|
User.transaction do
users.each do |user|
- rotator = KeyRotator::AttributeEncryption.new(user)
- rotator.rotate
- progress&.increment
+ begin
+ rotator = KeyRotator::AttributeEncryption.new(user)
+ rotator.rotate
+ progress&.increment
+ rescue StandardError => err # Don't use user.email in output...
+ Kernel.puts "Error with user id:#{user.id} #{err.message} #{err.backtrace}"
+ end
end
end
end
diff --git a/lib/user_flow_exporter.rb b/lib/user_flow_exporter.rb
index 0bc8af45368..15f049c56f0 100644
--- a/lib/user_flow_exporter.rb
+++ b/lib/user_flow_exporter.rb
@@ -89,4 +89,4 @@ def self.massage_assets(dir)
end
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/controllers/account_reset/cancel_controller_spec.rb b/spec/controllers/account_reset/cancel_controller_spec.rb
index 521243c1c37..2cc508be842 100644
--- a/spec/controllers/account_reset/cancel_controller_spec.rb
+++ b/spec/controllers/account_reset/cancel_controller_spec.rb
@@ -1,31 +1,66 @@
require 'rails_helper'
describe AccountReset::CancelController do
+ let(:user) { create(:user, :signed_up, phone: '+1 (703) 555-0000') }
+ before do
+ TwilioService::Utils.telephony_service = FakeSms
+ end
+
describe '#cancel' do
it 'logs a good token to the analytics' do
- user = create(:user)
AccountResetService.new(user).create_request
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :cancel, token_valid: true})
+ with(Analytics::ACCOUNT_RESET,
+ event: :cancel, token_valid: true, user_id: user.uuid)
- post :cancel, params: {token:AccountResetRequest.all[0].request_token}
+ post :cancel, params: { token: AccountResetRequest.all[0].request_token }
end
it 'logs a bad token to the analytics' do
-
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :cancel, token_valid: false})
+ with(Analytics::ACCOUNT_RESET, event: :cancel, token_valid: false)
- post :cancel, params: {token:'FOO'}
+ post :cancel, params: { token: 'FOO' }
end
it 'redirects to the root' do
-
post :cancel
expect(response).to redirect_to root_url
end
+
+ it 'sends an SMS if there is a phone' do
+ AccountResetService.new(user).create_request
+ allow(SmsAccountResetCancellationNotifierJob).to receive(:perform_now)
+
+ post :cancel, params: { token: AccountResetRequest.all[0].request_token }
+
+ expect(SmsAccountResetCancellationNotifierJob).to have_received(:perform_now).with(
+ phone: user.phone
+ )
+ end
+
+ it 'does not send an SMS if there is no phone' do
+ AccountResetService.new(user).create_request
+ allow(SmsAccountResetCancellationNotifierJob).to receive(:perform_now)
+ user.phone = nil
+ user.save!
+
+ post :cancel, params: { token: AccountResetRequest.all[0].request_token }
+
+ expect(SmsAccountResetCancellationNotifierJob).to_not have_received(:perform_now)
+ end
+
+ it 'sends an email' do
+ AccountResetService.new(user).create_request
+
+ @mailer = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
+ allow(UserMailer).to receive(:account_reset_cancel).with(user.email).
+ and_return(@mailer)
+
+ post :cancel, params: { token: AccountResetRequest.all[0].request_token }
+ end
end
end
diff --git a/spec/controllers/account_reset/delete_account_controller_spec.rb b/spec/controllers/account_reset/delete_account_controller_spec.rb
index e045fcccc91..c9c5030b982 100644
--- a/spec/controllers/account_reset/delete_account_controller_spec.rb
+++ b/spec/controllers/account_reset/delete_account_controller_spec.rb
@@ -10,7 +10,7 @@
session[:granted_token] = AccountResetRequest.all[0].granted_token
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :delete, token_valid: true})
+ with(Analytics::ACCOUNT_RESET, event: :delete, token_valid: true, user_id: user.uuid)
delete :delete
end
@@ -18,9 +18,9 @@
it 'logs a bad token to the analytics' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :delete, token_valid: false})
+ with(Analytics::ACCOUNT_RESET, event: :delete, token_valid: false)
- delete :delete, params: {token:'FOO'}
+ delete :delete, params: { token: 'FOO' }
end
it 'redirects to root if there is no token' do
@@ -36,13 +36,13 @@
AccountResetService.new(user).create_request
AccountResetService.new(user).grant_request
- get :show, params: {token: AccountResetRequest.all[0].granted_token}
+ get :show, params: { token: AccountResetRequest.all[0].granted_token }
expect(response).to redirect_to(account_reset_delete_account_url)
end
it 'redirects to root if the token is bad' do
- get :show, params: {token: 'FOO'}
+ get :show, params: { token: 'FOO' }
expect(response).to redirect_to(root_url)
end
diff --git a/spec/controllers/account_reset/report_fraud_controller_spec.rb b/spec/controllers/account_reset/report_fraud_controller_spec.rb
index 8268c91bb3f..383378f1fae 100644
--- a/spec/controllers/account_reset/report_fraud_controller_spec.rb
+++ b/spec/controllers/account_reset/report_fraud_controller_spec.rb
@@ -8,22 +8,20 @@
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :fraud, token_valid: true})
+ with(Analytics::ACCOUNT_RESET, event: :fraud, token_valid: true)
- post :update, params: {token:AccountResetRequest.all[0].request_token}
+ post :update, params: { token: AccountResetRequest.all[0].request_token }
end
it 'logs a bad token to the analytics' do
-
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :fraud, token_valid: false})
+ with(Analytics::ACCOUNT_RESET, event: :fraud, token_valid: false)
- post :update, params: {token:'FOO'}
+ post :update, params: { token: 'FOO' }
end
it 'redirects to the root' do
-
post :update
expect(response).to redirect_to root_url
end
diff --git a/spec/controllers/account_reset/request_controller_spec.rb b/spec/controllers/account_reset/request_controller_spec.rb
index d002d1675ff..9fb3678f3a7 100644
--- a/spec/controllers/account_reset/request_controller_spec.rb
+++ b/spec/controllers/account_reset/request_controller_spec.rb
@@ -27,12 +27,12 @@
describe '#create' do
it 'logs the request in the analytics' do
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
sign_in_before_2fa
stub_analytics
expect(@analytics).to receive(:track_event).
- with(Analytics::ACCOUNT_RESET, {event: :request})
+ with(Analytics::ACCOUNT_RESET, event: :request)
post :create
end
diff --git a/spec/controllers/idv/confirmations_controller_spec.rb b/spec/controllers/idv/confirmations_controller_spec.rb
index dd77041d4f0..adb891e083c 100644
--- a/spec/controllers/idv/confirmations_controller_spec.rb
+++ b/spec/controllers/idv/confirmations_controller_spec.rb
@@ -1,5 +1,4 @@
require 'rails_helper'
-require 'proofer/vendor/mock'
describe Idv::ConfirmationsController do
include SamlAuthHelper
diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb
index 09393049665..f0a6f8c9e0b 100644
--- a/spec/controllers/idv/review_controller_spec.rb
+++ b/spec/controllers/idv/review_controller_spec.rb
@@ -1,7 +1,5 @@
require 'rails_helper'
-require 'proofer/vendor/mock'
-
describe Idv::ReviewController do
let(:user) do
create(
diff --git a/spec/controllers/idv/usps_controller_spec.rb b/spec/controllers/idv/usps_controller_spec.rb
index e703016161d..0214d4be532 100644
--- a/spec/controllers/idv/usps_controller_spec.rb
+++ b/spec/controllers/idv/usps_controller_spec.rb
@@ -1,7 +1,5 @@
require 'rails_helper'
-require 'proofer/vendor/mock'
-
describe Idv::UspsController do
let(:user) { create(:user) }
diff --git a/spec/controllers/two_factor_authentication/options_controller_spec.rb b/spec/controllers/two_factor_authentication/options_controller_spec.rb
new file mode 100644
index 00000000000..a2ad7631828
--- /dev/null
+++ b/spec/controllers/two_factor_authentication/options_controller_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+describe TwoFactorAuthentication::OptionsController do
+ describe '#index' do
+ it 'renders the page' do
+ sign_in_before_2fa
+
+ get :index
+
+ expect(response).to render_template(:index)
+ end
+
+ it 'logs an analytics event' do
+ sign_in_before_2fa
+ stub_analytics
+
+ expect(@analytics).to receive(:track_event).
+ with(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST_VISIT)
+
+ get :index
+ end
+ end
+
+ describe '#create' do
+ before { sign_in_before_2fa }
+
+ it 'redirects to login_two_factor_url if user selects sms' do
+ post :create, params: { two_factor_options_form: { selection: 'sms' } }
+
+ expect(response).to redirect_to otp_send_url( \
+ otp_delivery_selection_form: { otp_delivery_preference: 'sms' }
+ )
+ end
+
+ it 'redirects to login_two_factor_url if user selects voice' do
+ post :create, params: { two_factor_options_form: { selection: 'voice' } }
+
+ expect(response).to redirect_to otp_send_url( \
+ otp_delivery_selection_form: { otp_delivery_preference: 'voice' }
+ )
+ end
+
+ it 'redirects to login_two_factor_piv_cac_url if user selects piv_cac' do
+ post :create, params: { two_factor_options_form: { selection: 'piv_cac' } }
+
+ expect(response).to redirect_to login_two_factor_piv_cac_url
+ end
+
+ it 'redirects to login_two_factor_authenticator_url if user selects auth_app' do
+ post :create, params: { two_factor_options_form: { selection: 'auth_app' } }
+
+ expect(response).to redirect_to login_two_factor_authenticator_url
+ end
+
+ it 'rerenders the page with errors on failure' do
+ post :create, params: { two_factor_options_form: { selection: 'foo' } }
+
+ expect(response).to render_template(:index)
+ end
+
+ it 'tracks analytics event' do
+ stub_sign_in_before_2fa
+ stub_analytics
+
+ result = {
+ selection: 'sms',
+ success: true,
+ errors: {},
+ }
+
+ expect(@analytics).to receive(:track_event).
+ with(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST, result)
+
+ post :create, params: { two_factor_options_form: { selection: 'sms' } }
+ end
+ end
+end
diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb
index c87b128c9da..85ef7036ebc 100644
--- a/spec/controllers/users/two_factor_authentication_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb
@@ -332,11 +332,18 @@ def index
}
twilio_error = "[HTTP 400] : error message\n\n"
+ twilio_error_hash = {
+ error: twilio_error,
+ code: '',
+ context: 'confirmation',
+ country: 'US',
+ }
+
expect(@analytics).to receive(:track_event).
with(Analytics::OTP_DELIVERY_SELECTION, analytics_hash)
expect(@analytics).to receive(:track_event).
- with(Analytics::TWILIO_PHONE_VALIDATION_FAILED, error: twilio_error, code: '')
+ with(Analytics::TWILIO_PHONE_VALIDATION_FAILED, twilio_error_hash)
get :send_code, params: { otp_delivery_selection_form: { otp_delivery_preference: 'sms' } }
end
@@ -345,7 +352,11 @@ def index
stub_analytics
code = 60_033
error_message = 'error'
- verify_error = PhoneVerification::VerifyError.new(code: code, message: error_message)
+ response = '{"error_code":"60004"}'
+ status = 400
+ verify_error = PhoneVerification::VerifyError.new(
+ code: code, message: error_message, status: status, response: response
+ )
allow(SmsOtpSenderJob).to receive(:perform_now).and_raise(verify_error)
analytics_hash = {
@@ -357,12 +368,20 @@ def index
country_code: '1',
area_code: '202',
}
+ twilio_error_hash = {
+ error: error_message,
+ code: code,
+ context: 'confirmation',
+ country: 'US',
+ status: status,
+ response: response,
+ }
expect(@analytics).to receive(:track_event).
with(Analytics::OTP_DELIVERY_SELECTION, analytics_hash)
expect(@analytics).to receive(:track_event).
- with(Analytics::TWILIO_PHONE_VALIDATION_FAILED, error: error_message, code: code)
+ with(Analytics::TWILIO_PHONE_VALIDATION_FAILED, twilio_error_hash)
get :send_code, params: { otp_delivery_selection_form: { otp_delivery_preference: 'sms' } }
end
diff --git a/spec/controllers/usps_upload_controller_spec.rb b/spec/controllers/usps_upload_controller_spec.rb
index 5a9d692af91..1fd295957e6 100644
--- a/spec/controllers/usps_upload_controller_spec.rb
+++ b/spec/controllers/usps_upload_controller_spec.rb
@@ -29,6 +29,8 @@
context 'on a federal workday' do
it 'runs the uploader' do
+ expect(controller).to receive(:today).and_return(Date.new(2018, 7, 3))
+
usps_uploader = instance_double(UspsUploader)
expect(usps_uploader).to receive(:run)
expect(UspsUploader).to receive(:new).and_return(usps_uploader)
diff --git a/spec/features/flows/sp_authentication_flows_spec.rb b/spec/features/flows/sp_authentication_flows_spec.rb
index 406395fcdae..2d5f9f66598 100644
--- a/spec/features/flows/sp_authentication_flows_spec.rb
+++ b/spec/features/flows/sp_authentication_flows_spec.rb
@@ -183,10 +183,6 @@
end
end
end
-
- # context 'when choosing to sign in' do
- # TODO: duplicate scenarios from Create Account here
- # end
end
context 'when LOA1' do
diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb
index 498f6e57f2d..eeea3adcfa3 100644
--- a/spec/features/openid_connect/openid_connect_spec.rb
+++ b/spec/features/openid_connect/openid_connect_spec.rb
@@ -5,7 +5,9 @@ class MockSession; end
shared_examples 'OpenID Connect' do |cloudhsm_enabled|
include IdvHelper
- before { enable_cloudhsm(cloudhsm_enabled) }
+ before do
+ enable_cloudhsm(cloudhsm_enabled)
+ end
after(:all) do
SamlIdp.configure { |config| SamlIdpEncryptionConfigurator.configure(config, false) }
end
diff --git a/spec/features/sign_in/two_factor_options_spec.rb b/spec/features/sign_in/two_factor_options_spec.rb
new file mode 100644
index 00000000000..65e95df3ed1
--- /dev/null
+++ b/spec/features/sign_in/two_factor_options_spec.rb
@@ -0,0 +1,183 @@
+require 'rails_helper'
+
+describe '2FA options when signing in' do
+ context 'when the user only has SMS configured' do
+ it 'only displays SMS, Voice and Personal key' do
+ user = create(:user, :signed_up, otp_delivery_preference: 'sms')
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.piv_cac')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.auth_app')
+ end
+ end
+
+ context 'when the user only has Voice configured' do
+ it 'only displays SMS, Voice and Personal key' do
+ user = create(:user, :signed_up, otp_delivery_preference: 'voice')
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.piv_cac')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.auth_app')
+ end
+ end
+
+ context 'when the user only has SMS configured with a number that we cannot call' do
+ it 'only displays SMS and Personal key' do
+ user = create(:user, :signed_up, otp_delivery_preference: 'sms', phone: '+12423270143')
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.piv_cac')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.auth_app')
+ end
+ end
+
+ context 'when the user only has TOTP configured' do
+ it 'only displays TOTP and Personal key' do
+ user = create(:user, :with_authentication_app)
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.auth_app')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.piv_cac')
+ end
+ end
+
+ context 'when the user only has PIV/CAC configured' do
+ it 'only displays PIV/CAC and Personal key' do
+ user = create(:user, :with_piv_or_cac, :with_personal_key)
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.auth_app')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.piv_cac')
+ end
+ end
+
+ context 'when the user only has SMS and TOTP configured' do
+ it 'only displays SMS, Voice, TOTP and Personal key' do
+ user = create(:user, :signed_up, :with_authentication_app)
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.auth_app')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.piv_cac')
+ end
+ end
+
+ context 'when the user only has SMS and PIV/CAC configured' do
+ it 'only displays SMS, Voice, PIV/CAC and Personal key' do
+ user = create(:user, :signed_up, :with_piv_or_cac)
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.auth_app')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.piv_cac')
+ end
+ end
+
+ context 'when the user only has TOTP and PIV/CAC configured' do
+ it 'only displays PIV/CAC, TOTP, and Personal key' do
+ user = create(:user, :with_authentication_app, :with_piv_or_cac)
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to_not have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.auth_app')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.piv_cac')
+ end
+ end
+
+ context 'when the user has SMS, TOTP and PIV/CAC configured' do
+ it 'only displays SMS, Voice, PIV/CAC, TOTP, and Personal key' do
+ user = create(:user, :signed_up, :with_authentication_app, :with_piv_or_cac)
+ sign_in_user(user)
+
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.sms')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.voice')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.personal_key')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.auth_app')
+ expect(page).
+ to have_content t('two_factor_authentication.login_options.piv_cac')
+ end
+ end
+end
diff --git a/spec/features/two_factor_authentication/change_factor_spec.rb b/spec/features/two_factor_authentication/change_factor_spec.rb
index c4ca885c936..fd6c34d8114 100644
--- a/spec/features/two_factor_authentication/change_factor_spec.rb
+++ b/spec/features/two_factor_authentication/change_factor_spec.rb
@@ -109,7 +109,7 @@
Timecop.travel(Figaro.env.reauthn_window.to_i + 1) do
visit manage_phone_path
complete_2fa_confirmation_without_entering_otp
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
expect(SmsOtpSenderJob).to have_received(:perform_later).
with(
@@ -175,6 +175,25 @@
end
end
+ context 'with SMS and number that Verify does not think is valid' do
+ it 'rescues the VerifyError' do
+ allow(SmsOtpSenderJob).to receive(:perform_later)
+ PhoneVerification.adapter = FakeAdapter
+ allow(FakeAdapter).to receive(:post).and_return(FakeAdapter::ErrorResponse.new)
+
+ user = create(:user, :signed_up, phone: '+17035551212')
+ visit new_user_session_path
+ sign_in_live_with_2fa(user)
+ visit manage_phone_path
+ select 'Morocco', from: 'user_phone_form_international_code'
+ fill_in 'user_phone_form_phone', with: '+212 661-289325'
+ click_button t('forms.buttons.submit.confirm_change')
+
+ expect(current_path).to eq manage_phone_path
+ expect(page).to have_content t('errors.messages.invalid_phone_number')
+ end
+ end
+
def complete_2fa_confirmation
complete_2fa_confirmation_without_entering_otp
click_submit_default
diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb
index 61cf0da6b8a..c48e27b06d6 100644
--- a/spec/features/two_factor_authentication/sign_in_spec.rb
+++ b/spec/features/two_factor_authentication/sign_in_spec.rb
@@ -69,7 +69,7 @@
location: 'Bahamas'
)
- click_on t('devise.two_factor_authentication.two_factor_choice_cancel')
+ click_on t('two_factor_authentication.choose_another_option')
expect(current_path).to eq two_factor_options_path
end
@@ -203,7 +203,7 @@ def submit_prefilled_otp_code
sign_in_before_2fa(user)
old_code = find('input[@name="code"]').value
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
new_code = find('input[@name="code"]').value
@@ -231,7 +231,7 @@ def submit_prefilled_otp_code
allow(VoiceOtpSenderJob).to receive(:perform_later)
- click_on t('links.two_factor_authentication.voice')
+ choose_another_security_option('voice')
expect(VoiceOtpSenderJob).to have_received(:perform_later)
end
@@ -285,7 +285,7 @@ def submit_prefilled_otp_code
sign_in_before_2fa(user)
Figaro.env.otp_delivery_blocklist_maxretry.to_i.times do
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
end
expect(page).to have_content t('titles.account_locked')
@@ -318,7 +318,7 @@ def submit_prefilled_otp_code
sign_in_before_2fa(user)
Figaro.env.otp_delivery_blocklist_maxretry.to_i.times do
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
end
expect(page).to have_content t('titles.account_locked')
@@ -340,7 +340,7 @@ def submit_prefilled_otp_code
sign_in_before_2fa(user)
(max_attempts - 1).times do
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
end
click_submit_default
@@ -357,7 +357,7 @@ def submit_prefilled_otp_code
expect(rate_limited_phone.reload.otp_send_count).to eq 1
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
@@ -375,14 +375,14 @@ def submit_prefilled_otp_code
max_attempts = Figaro.env.otp_delivery_blocklist_maxretry.to_i
sign_in_before_2fa(first_user)
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
visit destroy_user_session_url
sign_in_before_2fa(second_user)
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
phone_fingerprint = Pii::Fingerprinter.fingerprint(first_user.phone)
rate_limited_phone = OtpRequestsTracker.find_by(phone_fingerprint: phone_fingerprint)
@@ -415,7 +415,7 @@ def submit_prefilled_otp_code
submit_2fa_setup_form_with_valid_phone
max_attempts.times do
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
end
expect(page).to have_content t('titles.account_locked')
@@ -479,15 +479,13 @@ def submit_prefilled_otp_code
expect(current_path).to eq login_two_factor_piv_cac_path
- expect(page).not_to have_link(t('links.two_factor_authentication.app'))
-
- click_link t('devise.two_factor_authentication.totp_fallback.sms_link_text')
+ choose_another_security_option('sms')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
visit login_two_factor_piv_cac_path
- click_link t('devise.two_factor_authentication.totp_fallback.voice_link_text')
+ choose_another_security_option('voice')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'voice')
end
@@ -498,7 +496,7 @@ def submit_prefilled_otp_code
expect(current_path).to eq login_two_factor_piv_cac_path
- click_link t('links.two_factor_authentication.app')
+ choose_another_security_option('auth_app')
expect(current_path).to eq login_two_factor_authenticator_path
end
@@ -562,6 +560,45 @@ def submit_prefilled_otp_code
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
end
end
+
+ context 'with SMS and international number that Verify does not think is valid' do
+ it 'rescues the VerifyError' do
+ allow(SmsOtpSenderJob).to receive(:perform_later) do |*args|
+ SmsOtpSenderJob.perform_now(*args)
+ end
+ PhoneVerification.adapter = FakeAdapter
+ allow(FakeAdapter).to receive(:post).and_return(FakeAdapter::ErrorResponse.new)
+
+ user = create(:user, :signed_up, phone: '+212 661-289324')
+ sign_in_user(user)
+
+ expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
+ expect(page).
+ to have_content t('errors.messages.phone_unsupported')
+ end
+ end
+
+ context 'user with Voice preference sends SMS, causing a Twilio error' do
+ it 'does not change their OTP delivery preference' do
+ allow(Figaro.env).to receive(:programmable_sms_countries).and_return('CA')
+ allow(VoiceOtpSenderJob).to receive(:perform_later)
+ allow(SmsOtpSenderJob).to receive(:perform_later) do |*args|
+ SmsOtpSenderJob.perform_now(*args)
+ end
+ PhoneVerification.adapter = FakeAdapter
+ allow(FakeAdapter).to receive(:post).and_return(FakeAdapter::ErrorResponse.new)
+
+ user = create(:user, :signed_up, phone: '+17035551212', otp_delivery_preference: 'voice')
+ sign_in_user(user)
+
+ expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'voice')
+
+ choose_another_security_option('sms')
+
+ expect(page).to have_content t('errors.messages.invalid_phone_number')
+ expect(user.reload.otp_delivery_preference).to eq 'voice'
+ end
+ end
end
describe 'when the user is not piv/cac enabled' do
@@ -578,13 +615,13 @@ def submit_prefilled_otp_code
user = create(:user, :with_authentication_app, :with_phone)
sign_in_before_2fa(user)
- click_link t('devise.two_factor_authentication.totp_fallback.sms_link_text')
+ choose_another_security_option('sms')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
visit login_two_factor_authenticator_path
- click_link t('devise.two_factor_authentication.totp_fallback.voice_link_text')
+ choose_another_security_option('voice')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'voice')
end
@@ -621,7 +658,6 @@ def submit_prefilled_otp_code
end
end
- # TODO: readd profile redirect, modal tests
describe 'signing in when user does not already have personal key' do
# For example, when migrating users from another DB
it 'displays personal key and redirects to profile' do
diff --git a/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb b/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb
index b125e56343c..9d0d05793a0 100644
--- a/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb
+++ b/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb
@@ -7,7 +7,7 @@
personal_key = PersonalKeyGenerator.new(user).create
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: personal_key)
@@ -26,7 +26,7 @@
personal_key = PersonalKeyGenerator.new(user).create
wrong_personal_key = personal_key.split('-').reverse.join
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: wrong_personal_key)
click_submit_default
diff --git a/spec/features/users/password_recovery_via_recovery_code_spec.rb b/spec/features/users/password_recovery_via_recovery_code_spec.rb
index 8e52bae7b68..410e5e4c954 100644
--- a/spec/features/users/password_recovery_via_recovery_code_spec.rb
+++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb
@@ -58,7 +58,7 @@
trigger_reset_password_and_click_email_link(user.email)
reset_password_and_sign_back_in(user, new_password)
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: personal_key)
click_submit_default
diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb
index f954c9ccfae..6826ff45371 100644
--- a/spec/features/users/sign_in_spec.rb
+++ b/spec/features/users/sign_in_spec.rb
@@ -412,7 +412,7 @@
user = create(:user, :signed_up)
old_personal_key = PersonalKeyGenerator.new(user).create
signin(user.email, user.password)
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: old_personal_key)
click_submit_default
visit account_path
diff --git a/spec/features/visitors/phone_confirmation_spec.rb b/spec/features/visitors/phone_confirmation_spec.rb
index a4b6fea9395..a717ea92c0c 100644
--- a/spec/features/visitors/phone_confirmation_spec.rb
+++ b/spec/features/visitors/phone_confirmation_spec.rb
@@ -30,7 +30,7 @@
end
it 'allows user to resend confirmation code' do
- click_link t('links.two_factor_authentication.resend_code.sms')
+ click_link t('links.two_factor_authentication.get_another_code')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
end
@@ -48,11 +48,9 @@
end
it 'informs the user that the OTP code is sent to the phone' do
- expect(page).to have_content(
- t('instructions.mfa.sms.confirm_code_html',
- number: '+1 (703) 555-5555',
- resend_code_link: t('links.two_factor_authentication.resend_code.sms'))
- )
+ expect(page).to have_content(t('instructions.mfa.sms.number_message',
+ number: '+1 703-555-5555',
+ expiration: Figaro.env.otp_valid_for))
end
end
@@ -67,11 +65,9 @@
it 'pretends the phone is valid and prompts to confirm the number' do
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
- expect(page).to have_content(
- t('instructions.mfa.sms.confirm_code_html',
- number: @existing_user.phone,
- resend_code_link: t('links.two_factor_authentication.resend_code.sms'))
- )
+ expect(page).to have_content(t('instructions.mfa.sms.number_message',
+ number: '+1 202-555-1212',
+ expiration: Figaro.env.otp_valid_for))
end
it 'does not confirm the new number with an invalid code' do
diff --git a/spec/forms/otp_delivery_selection_form_spec.rb b/spec/forms/otp_delivery_selection_form_spec.rb
index d281a5dcda7..d1a50d84ae0 100644
--- a/spec/forms/otp_delivery_selection_form_spec.rb
+++ b/spec/forms/otp_delivery_selection_form_spec.rb
@@ -66,56 +66,38 @@
end
end
- context 'with authentication context' do
- context 'when otp_delivery_preference is the same as the user otp_delivery_preference' do
- it 'does not update the user' do
- user = build_stubbed(:user, otp_delivery_preference: 'sms')
- form = OtpDeliverySelectionForm.new(user, phone_to_deliver_to, 'authentication')
-
- expect(UpdateUser).to_not receive(:new)
-
- form.submit(otp_delivery_preference: 'sms')
- end
- end
-
- context 'when otp_delivery_preference is different from the user otp_delivery_preference' do
- it 'updates the user' do
- user = build_stubbed(:user, otp_delivery_preference: 'voice')
- form = OtpDeliverySelectionForm.new(user, phone_to_deliver_to, 'authentication')
- attributes = { otp_delivery_preference: 'sms' }
+ context 'with voice preference and unsupported phone' do
+ it 'changes the otp_delivery_preference to sms' do
+ user = build_stubbed(:user, otp_delivery_preference: 'voice')
+ form = OtpDeliverySelectionForm.new(
+ user,
+ '+12423270143',
+ 'authentication'
+ )
+ attributes = { otp_delivery_preference: 'sms' }
- updated_user = instance_double(UpdateUser)
- allow(UpdateUser).to receive(:new).
- with(user: user, attributes: attributes).and_return(updated_user)
+ updated_user = instance_double(UpdateUser)
+ allow(UpdateUser).to receive(:new).
+ with(user: user, attributes: attributes).and_return(updated_user)
- expect(updated_user).to receive(:call)
+ expect(updated_user).to receive(:call)
- form.submit(otp_delivery_preference: 'sms')
- end
+ form.submit(otp_delivery_preference: 'voice')
end
end
- context 'with idv context' do
- context 'when otp_delivery_preference is the same as the user otp_delivery_preference' do
- it 'does not update the user' do
- user = build_stubbed(:user, otp_delivery_preference: 'sms')
- form = OtpDeliverySelectionForm.new(user, phone_to_deliver_to, 'idv')
-
- expect(UpdateUser).to_not receive(:new)
-
- form.submit(otp_delivery_preference: 'sms')
- end
- end
-
- context 'when otp_delivery_preference is different from the user otp_delivery_preference' do
- it 'does not update the user' do
- user = build_stubbed(:user, otp_delivery_preference: 'voice')
- form = OtpDeliverySelectionForm.new(user, phone_to_deliver_to, 'idv')
+ context 'with voice preference and supported phone' do
+ it 'does not change the otp_delivery_preference to sms' do
+ user = build_stubbed(:user, otp_delivery_preference: 'voice')
+ form = OtpDeliverySelectionForm.new(
+ user,
+ '+17035551212',
+ 'authentication'
+ )
- expect(UpdateUser).to_not receive(:new)
+ expect(UpdateUser).to_not receive(:new)
- form.submit(otp_delivery_preference: 'sms')
- end
+ form.submit(otp_delivery_preference: 'voice')
end
end
end
diff --git a/spec/forms/sms_form_spec.rb b/spec/forms/sms_form_spec.rb
new file mode 100644
index 00000000000..ae0a1e9d0df
--- /dev/null
+++ b/spec/forms/sms_form_spec.rb
@@ -0,0 +1,46 @@
+require 'rails_helper'
+
+describe SmsForm do
+ describe '#submit' do
+ let(:url) { 'https://example.com' }
+ let(:signature) { 'signature' }
+
+ before do
+ # Only testing our validation here; service spec tests Twilio msg failing
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(true)
+ )
+ end
+
+ context 'when the form is valid' do
+ it 'returns FormResponse with success: true' do
+ good_params = { Body: 'STOP', FromCountry: 'US', MessageSid: '1' }
+ message = TwilioService::Sms::Request.new(url, good_params, signature)
+ form = SmsForm.new(message)
+
+ result = instance_double(FormResponse)
+ extra = { message_sid: '1', from_country: 'US' }
+
+ expect(FormResponse).to receive(:new).
+ with(success: true, errors: {}, extra: extra).and_return(result)
+ expect(form.submit).to eq result
+ end
+ end
+
+ context 'when the form is invalid' do
+ it 'returns FormResponse with success: false' do
+ bad_params = { Body: 'SPORK', FromCountry: 'US', MessageSid: '1' }
+ message = TwilioService::Sms::Request.new(url, bad_params, signature)
+ form = SmsForm.new(message)
+ errors = { base: [t('errors.messages.twilio_inbound_sms_invalid')] }
+
+ result = instance_double(FormResponse)
+ extra = { message_sid: '1', from_country: 'US' }
+
+ expect(FormResponse).to receive(:new).
+ with(success: false, errors: errors, extra: extra).and_return(result)
+ expect(form.submit).to eq result
+ end
+ end
+ end
+end
diff --git a/spec/forms/two_factor_login_options_form_spec.rb b/spec/forms/two_factor_login_options_form_spec.rb
new file mode 100644
index 00000000000..2ae1f264cc6
--- /dev/null
+++ b/spec/forms/two_factor_login_options_form_spec.rb
@@ -0,0 +1,43 @@
+require 'rails_helper'
+
+describe TwoFactorLoginOptionsForm do
+ subject do
+ TwoFactorLoginOptionsForm.new(
+ build_stubbed(:user)
+ )
+ end
+
+ describe '#submit' do
+ context 'when the form is valid' do
+ it 'returns true for success?' do
+ extra = {
+ selection: 'sms',
+ }
+
+ result = instance_double(FormResponse)
+
+ expect(FormResponse).to receive(:new).
+ with(success: true, errors: {}, extra: extra).and_return(result)
+ expect(subject.submit(selection: 'sms')).to eq result
+ end
+ end
+
+ context 'when the form is invalid' do
+ it 'returns false for success? and includes errors' do
+ errors = {
+ selection: ['is not included in the list'],
+ }
+
+ extra = {
+ selection: 'foo',
+ }
+
+ result = instance_double(FormResponse)
+
+ expect(FormResponse).to receive(:new).
+ with(success: false, errors: errors, extra: extra).and_return(result)
+ expect(subject.submit(selection: 'foo')).to eq result
+ end
+ end
+ end
+end
diff --git a/spec/jobs/sms_account_reset_cancellation_notifier_job_spec.rb b/spec/jobs/sms_account_reset_cancellation_notifier_job_spec.rb
new file mode 100644
index 00000000000..2960013b35d
--- /dev/null
+++ b/spec/jobs/sms_account_reset_cancellation_notifier_job_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe SmsAccountResetCancellationNotifierJob do
+ include Features::ActiveJobHelper
+
+ describe '.perform' do
+ before do
+ reset_job_queues
+ TwilioService::Utils.telephony_service = FakeSms
+ FakeSms.messages = []
+ end
+
+ subject(:perform) do
+ SmsAccountResetCancellationNotifierJob.perform_now(
+ phone: '+1 (888) 555-5555'
+ )
+ end
+
+ it 'sends a message to the mobile number', twilio: true do
+ allow(Figaro.env).to receive(:twilio_messaging_service_sid).and_return('fake_sid')
+
+ TwilioService::Utils.telephony_service = FakeSms
+
+ perform
+
+ messages = FakeSms.messages
+
+ expect(messages.size).to eq(1)
+
+ msg = messages.first
+
+ expect(msg.messaging_service_sid).to eq('fake_sid')
+ expect(msg.to).to eq('+1 (888) 555-5555')
+ expect(msg.body).
+ to eq(I18n.t('jobs.sms_account_reset_cancel_job.message', app: APP_NAME))
+ end
+ end
+end
diff --git a/spec/jobs/sms_account_reset_notifier_job_spec.rb b/spec/jobs/sms_account_reset_notifier_job_spec.rb
index 1c220653cd5..d80cdfe9ee6 100644
--- a/spec/jobs/sms_account_reset_notifier_job_spec.rb
+++ b/spec/jobs/sms_account_reset_notifier_job_spec.rb
@@ -7,7 +7,7 @@
describe '.perform' do
before do
reset_job_queues
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
FakeSms.messages = []
end
@@ -21,7 +21,7 @@
it 'sends a message containing the cancel link to the mobile number', twilio: true do
allow(Figaro.env).to receive(:twilio_messaging_service_sid).and_return('fake_sid')
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
perform
@@ -33,8 +33,10 @@
expect(msg.messaging_service_sid).to eq('fake_sid')
expect(msg.to).to eq('+1 (888) 555-5555')
- expect(msg.body).to eq(I18n.t('jobs.sms_account_reset_notifier_job.message', app: APP_NAME,
- cancel_link: account_reset_cancel_url(token: 'UUID1')))
+ cancel_link = account_reset_cancel_url(token: 'UUID1')
+ expect(msg.body).
+ to eq(I18n.t('jobs.sms_account_reset_notifier_job.message', app: APP_NAME,
+ cancel_link: cancel_link))
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/jobs/sms_otp_sender_job_spec.rb b/spec/jobs/sms_otp_sender_job_spec.rb
index 288fa3bd67a..0ae550cc039 100644
--- a/spec/jobs/sms_otp_sender_job_spec.rb
+++ b/spec/jobs/sms_otp_sender_job_spec.rb
@@ -6,7 +6,7 @@
describe '.perform' do
before do
reset_job_queues
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
FakeSms.messages = []
end
@@ -23,7 +23,7 @@
it 'sends a message containing the OTP code to the mobile number', twilio: true do
allow(Figaro.env).to receive(:twilio_messaging_service_sid).and_return('fake_sid')
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
perform
@@ -44,7 +44,7 @@
allow(I18n).to receive(:locale).and_return(:en).at_least(:once)
allow(Devise).to receive(:direct_otp_valid_for).and_return(4.minutes)
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
perform
@@ -83,7 +83,7 @@
end
end
- context 'when the parsed country of the phone number is not US' do
+ context 'when the phone number country is not in the programmable_sms_countries list' do
it 'sends the SMS via PhoneVerification class' do
PhoneVerification.adapter = FakeAdapter
phone = '+1 787-327-0143'
@@ -104,5 +104,30 @@
)
end
end
+
+ context 'when the phone number country is in the programmable_sms_countries list' do
+ it 'sends the SMS via TwilioService' do
+ allow(Figaro.env).to receive(:programmable_sms_countries).and_return('US,CA,FR')
+ phone = '+33 661 32 70 14'
+ service = instance_double(TwilioService::Utils)
+ code = '123456'
+
+ expect(TwilioService::Utils).to receive(:new).and_return(service)
+ expect(service).to receive(:send_sms).with(
+ to: phone,
+ body: I18n.t(
+ 'jobs.sms_otp_sender_job.message',
+ code: code, app: APP_NAME, expiration: Devise.direct_otp_valid_for.to_i / 60
+ )
+ )
+
+ SmsOtpSenderJob.perform_now(
+ code: code,
+ phone: phone,
+ otp_created_at: otp_created_at,
+ locale: 'fr'
+ )
+ end
+ end
end
end
diff --git a/spec/jobs/voice_otp_sender_job_spec.rb b/spec/jobs/voice_otp_sender_job_spec.rb
index ef2275176aa..490accc8d26 100644
--- a/spec/jobs/voice_otp_sender_job_spec.rb
+++ b/spec/jobs/voice_otp_sender_job_spec.rb
@@ -5,7 +5,7 @@
describe '.perform' do
before do
- TwilioService.telephony_service = FakeVoiceCall
+ TwilioService::Utils.telephony_service = FakeVoiceCall
FakeVoiceCall.calls = []
end
diff --git a/spec/lib/tasks/rotate_rake_spec.rb b/spec/lib/tasks/rotate_rake_spec.rb
index ae7fb955892..829902cc56f 100644
--- a/spec/lib/tasks/rotate_rake_spec.rb
+++ b/spec/lib/tasks/rotate_rake_spec.rb
@@ -2,14 +2,18 @@
require 'rake'
describe 'rotate' do
+ let(:user) { create(:user, phone: '703-555-5555') }
+ before do
+ Rake.application.rake_require('lib/tasks/rotate', [Rails.root.to_s])
+ Rake::Task.define_task(:environment)
+ ENV['PROGRESS'] = 'no'
+ end
+ after do
+ ENV['PROGRESS'] = 'yes'
+ end
+
describe 'attribute_encryption_key' do
it 'runs successfully' do
- prev_progress = ENV['PROGRESS']
- ENV['PROGRESS'] = 'no'
- Rake.application.rake_require('lib/tasks/rotate', [Rails.root.to_s])
- Rake::Task.define_task(:environment)
-
- user = create(:user, phone: '703-555-5555')
old_email = user.email
old_phone = user.phone
old_encrypted_email = user.encrypted_email
@@ -17,14 +21,29 @@
rotate_attribute_encryption_key
- Rake::Task['rotate:attribute_encryption_key'].invoke
+ Rake::Task['rotate:attribute_encryption_key'].execute
user.reload
expect(user.phone).to eq old_phone
expect(user.email).to eq old_email
expect(user.encrypted_email).to_not eq old_encrypted_email
expect(user.encrypted_phone).to_not eq old_encrypted_phone
- ENV['PROGRESS'] = prev_progress
+ end
+
+ it 'does not raise an exception when encrypting/decrypting a user' do
+ allow_any_instance_of(User).to receive(:email).and_raise(StandardError)
+
+ expect do
+ Rake::Task['rotate:attribute_encryption_key'].execute
+ end.to_not raise_error
+ end
+
+ it 'outputs diagnostic information on users that throw exceptions ' do
+ allow_any_instance_of(User).to receive(:email).and_raise(StandardError)
+
+ expect do
+ Rake::Task['rotate:attribute_encryption_key'].execute
+ end.to output(/Error with user id:#{user.id}/).to_stdout
end
end
end
diff --git a/spec/lib/worker_health_checker_spec.rb b/spec/lib/worker_health_checker_spec.rb
index 3c67542ffba..8ed30ec5884 100644
--- a/spec/lib/worker_health_checker_spec.rb
+++ b/spec/lib/worker_health_checker_spec.rb
@@ -12,7 +12,6 @@
end
def create_sidekiq_queues(*queues)
- # TODO: find an API to use rather than manually mess with redis?
Sidekiq.redis do |redis|
queues.each do |queue|
redis.sadd('queues', queue)
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 13e6c1ac0c2..beb7db5bab6 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -145,9 +145,12 @@ def expect_email_body_to_have_help_and_contact_links
end
it 'renders the body' do
- expect(mail.html_part.body).to have_content(strip_tags( \
- t('user_mailer.account_reset_request.intro', \
- cancel_account_reset: t('user_mailer.account_reset_granted.cancel_link_text'))))
+ reset_text = t('user_mailer.account_reset_granted.cancel_link_text')
+ expect(mail.html_part.body).to have_content(
+ strip_tags(
+ t('user_mailer.account_reset_request.intro', cancel_account_reset: reset_text)
+ )
+ )
end
end
@@ -184,7 +187,8 @@ def expect_email_body_to_have_help_and_contact_links
end
it 'renders the body' do
- expect(mail.html_part.body).to have_content(strip_tags(t('user_mailer.account_reset_complete.intro')))
+ expect(mail.html_part.body).
+ to have_content(strip_tags(t('user_mailer.account_reset_complete.intro')))
end
end
diff --git a/spec/models/account_reset_request_spec.rb b/spec/models/account_reset_request_spec.rb
index 41ede864ac7..fdc19d3fd93 100644
--- a/spec/models/account_reset_request_spec.rb
+++ b/spec/models/account_reset_request_spec.rb
@@ -41,7 +41,9 @@
end
it 'returns the record if the token is valid' do
- arr = AccountResetRequest.create(id: 1, user_id: 2, granted_token: '123', granted_at: Time.zone.now)
+ arr = AccountResetRequest.create(
+ id: 1, user_id: 2, granted_token: '123', granted_at: Time.zone.now
+ )
expect(AccountResetRequest.from_valid_granted_token('123')).to eq(arr)
end
diff --git a/spec/presenters/two_factor_auth_code/authenticator_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/authenticator_delivery_presenter_spec.rb
index abd1d8621c3..7f4d9f4e860 100644
--- a/spec/presenters/two_factor_auth_code/authenticator_delivery_presenter_spec.rb
+++ b/spec/presenters/two_factor_auth_code/authenticator_delivery_presenter_spec.rb
@@ -1,16 +1,28 @@
require 'rails_helper'
describe TwoFactorAuthCode::AuthenticatorDeliveryPresenter do
+ let(:view) { ActionController::Base.new.view_context }
+ let(:presenter) do
+ TwoFactorAuthCode::AuthenticatorDeliveryPresenter.
+ new(data: {}, view: view)
+ end
+
+ describe '#header' do
+ it 'supplies a header' do
+ expect(presenter.header).to eq(t('devise.two_factor_authentication.totp_header_text'))
+ end
+ end
+
+ describe '#fallback_question' do
+ it 'supplies a fallback_question' do
+ expect(presenter.fallback_question).to \
+ eq(t('two_factor_authentication.totp_fallback.question'))
+ end
+ end
+
it 'handles multiple locales' do
I18n.available_locales.each do |locale|
I18n.locale = locale
- presenter.fallback_links.each do |html|
- if locale == :en
- expect(html).not_to match(%r{href="/en/})
- else
- expect(html).to match(%r{href="/#{locale}/})
- end
- end
if locale == :en
expect(presenter.cancel_link).not_to match(%r{/en/})
else
@@ -18,9 +30,4 @@
end
end
end
-
- def presenter
- TwoFactorAuthCode::AuthenticatorDeliveryPresenter.
- new(data: {}, view: ActionController::Base.new.view_context)
- end
end
diff --git a/spec/presenters/two_factor_auth_code/personal_key_presenter_spec.rb b/spec/presenters/two_factor_auth_code/personal_key_presenter_spec.rb
new file mode 100644
index 00000000000..8e67e556238
--- /dev/null
+++ b/spec/presenters/two_factor_auth_code/personal_key_presenter_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+describe TwoFactorAuthCode::PersonalKeyPresenter do
+ include Rails.application.routes.url_helpers
+
+ let(:presenter) do
+ TwoFactorAuthCode::PersonalKeyPresenter.new
+ end
+
+ describe '#fallback_question' do
+ it 'returns the fallback question' do
+ expect(presenter.fallback_question).to eq \
+ t('two_factor_authentication.personal_key_fallback.question')
+ end
+ end
+
+ describe '#help_text' do
+ it 'returns blank' do
+ expect(presenter.help_text).to eq ''
+ end
+ end
+end
diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
index 3fdb8a3c622..878b0759c4b 100644
--- a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
+++ b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
@@ -52,127 +52,6 @@
end
end
- describe '#fallback_links' do
- it 'does not list an account reset link when the phone is unconfirmed' do
- allow(presenter).to receive(:unconfirmed_phone).and_return(true)
-
- expect(presenter.fallback_links.join(' ')).to_not include(account_reset_delete_account_link)
- end
-
- it 'does not list an account reset link when the feature is disabled' do
- allow(presenter).to receive(:unconfirmed_phone).and_return(false)
- allow(Figaro.env).to receive(:account_reset_enabled).and_return('false')
-
- expect(presenter.fallback_links.join(' ')).to_not include(account_reset_delete_account_link)
- end
-
- it 'shows an account reset link when the user has not requested a delete' do
- allow(presenter).to receive(:unconfirmed_phone).and_return(false)
- allow(Figaro.env).to receive(:account_reset_enabled).and_return('enabled')
-
- expect(presenter.fallback_links.join(' ')).to include(account_reset_delete_account_link)
- end
-
- it 'shows a cancel link when the user has requested a delete' do
- allow(presenter).to receive(:unconfirmed_phone).and_return(false)
- allow(presenter).to receive(:account_reset_token).and_return('foo')
- allow(Figaro.env).to receive(:account_reset_enabled).and_return('enabled')
-
- expect(presenter.fallback_links.join(' ')).to include(account_reset_cancel_link('foo'))
- end
-
- it 'handles multiple locales' do
- I18n.available_locales.each do |locale|
- presenter_for_locale = presenter_with_locale(locale)
- I18n.locale = locale
- presenter_for_locale.fallback_links.each do |html|
- if locale == :en
- expect(html).not_to match(%r{href="/en/})
- else
- expect(html).to match(%r{href="/#{locale}/})
- end
- end
- if locale == :en
- expect(presenter_for_locale.cancel_link).not_to match(%r{/en/})
- else
- expect(presenter_for_locale.cancel_link).to match(%r{/#{locale}/})
- end
- end
- end
-
- context 'with totp enabled' do
- before do
- data[:totp_enabled] = true
- end
-
- context 'voice otp delivery supported' do
- it 'renders an auth app fallback link' do
- expect(presenter.fallback_links.join(' ')).to include(
- I18n.t('links.two_factor_authentication.app')
- )
- end
-
- it 'renders a voice otp link' do
- expect(presenter.fallback_links.join(' ')).to include(
- I18n.t('links.two_factor_authentication.voice')
- )
- end
- end
-
- context 'voice otp deliver unsupported' do
- before do
- data[:voice_otp_delivery_unsupported] = true
- end
-
- it 'renders an auth app fallback link' do
- expect(presenter.fallback_links.join(' ')).to include(
- I18n.t('links.two_factor_authentication.app')
- )
- end
-
- it 'does not render a voice otp link' do
- expect(presenter.fallback_links.join(' ')).to_not include(
- I18n.t('links.two_factor_authentication.voice')
- )
- end
- end
- end
-
- context 'without totp enabled' do
- context 'voice otp delivery supported' do
- it 'does not render an auth app fallback link' do
- expect(presenter.fallback_links.join(' ')).to_not include(
- I18n.t('links.two_factor_authentication.app')
- )
- end
-
- it 'renders a voice otp link' do
- expect(presenter.fallback_links.join(' ')).to include(
- I18n.t('links.two_factor_authentication.voice')
- )
- end
- end
-
- context 'voice otp deliver unsupported' do
- before do
- data[:voice_otp_delivery_unsupported] = true
- end
-
- it 'does not render an auth app fallback link' do
- expect(presenter.fallback_links.join(' ')).to_not include(
- I18n.t('links.two_factor_authentication.app')
- )
- end
-
- it 'does not render a voice otp link' do
- expect(presenter.fallback_links.join(' ')).to_not include(
- I18n.t('links.two_factor_authentication.voice')
- )
- end
- end
- end
- end
-
describe '#phone_number_message' do
it 'specifies when the code will expire' do
text = t(
diff --git a/spec/presenters/two_factor_auth_code/piv_cac_authentication_presenter_spec.rb b/spec/presenters/two_factor_auth_code/piv_cac_authentication_presenter_spec.rb
index 615ab590732..9449d4137e0 100644
--- a/spec/presenters/two_factor_auth_code/piv_cac_authentication_presenter_spec.rb
+++ b/spec/presenters/two_factor_auth_code/piv_cac_authentication_presenter_spec.rb
@@ -34,28 +34,6 @@ def presenter_with(arguments = {}, view = ActionController::Base.new.view_contex
it { expect(presenter.piv_cac_capture_text).to eq expected_text }
end
- describe '#fallback_links' do
- context 'with phone enabled' do
- let(:presenter) do
- presenter_with(reauthn: reauthn, user_email: user_email, phone_enabled: true)
- end
-
- it 'has two options' do
- expect(presenter.fallback_links.count).to eq 2
- end
- end
-
- context 'with phone disabled' do
- let(:presenter) do
- presenter_with(reauthn: reauthn, user_email: user_email, phone_enabled: false)
- end
-
- it 'has one option' do
- expect(presenter.fallback_links.count).to eq 1
- end
- end
- end
-
describe '#cancel_link' do
let(:locale) { LinkLocaleResolver.locale }
diff --git a/spec/presenters/two_factor_login_options_presenter_spec.rb b/spec/presenters/two_factor_login_options_presenter_spec.rb
new file mode 100644
index 00000000000..7220d2926ba
--- /dev/null
+++ b/spec/presenters/two_factor_login_options_presenter_spec.rb
@@ -0,0 +1,43 @@
+require 'rails_helper'
+
+describe TwoFactorLoginOptionsPresenter do
+ include Rails.application.routes.url_helpers
+
+ let(:user) { User.new }
+ let(:view) { ActionController::Base.new.view_context }
+ let(:presenter) do
+ TwoFactorLoginOptionsPresenter.new(user, view, nil)
+ end
+
+ it 'supplies a title' do
+ expect(presenter.title).to eq \
+ t('two_factor_authentication.login_options_title')
+ end
+
+ it 'supplies a heading' do
+ expect(presenter.heading).to eq \
+ t('two_factor_authentication.login_options_title')
+ end
+
+ it 'supplies a cancel link when there is an account reset token' do
+ allow_any_instance_of(TwoFactorLoginOptionsPresenter).to \
+ receive(:account_reset_token).and_return('foo')
+ expect(presenter.account_reset_or_cancel_link).to eq \
+ t('devise.two_factor_authentication.account_reset.pending_html',
+ cancel_link: view.link_to(
+ t('devise.two_factor_authentication.account_reset.cancel_link'),
+ account_reset_cancel_url(token: 'foo')
+ ))
+ end
+
+ it 'supplies a reset link when there is no account reset token' do
+ allow_any_instance_of(TwoFactorLoginOptionsPresenter).to \
+ receive(:account_reset_token).and_return(nil)
+ expect(presenter.account_reset_or_cancel_link).to eq \
+ t('devise.two_factor_authentication.account_reset.text_html',
+ link: view.link_to(
+ t('devise.two_factor_authentication.account_reset.link'),
+ account_reset_request_path(locale: LinkLocaleResolver.locale)
+ ))
+ end
+end
diff --git a/spec/requests/invalid_sign_in_params_spec.rb b/spec/requests/invalid_sign_in_params_spec.rb
new file mode 100644
index 00000000000..c0dd3dc1b33
--- /dev/null
+++ b/spec/requests/invalid_sign_in_params_spec.rb
@@ -0,0 +1,7 @@
+require 'rails_helper'
+
+describe 'visiting sign in page with invalid user params' do
+ it 'does not raise an exception' do
+ get new_user_session_path, params: { user: 'test@test.com' }
+ end
+end
diff --git a/spec/requests/sms_spec.rb b/spec/requests/sms_spec.rb
new file mode 100644
index 00000000000..06cd7f1a063
--- /dev/null
+++ b/spec/requests/sms_spec.rb
@@ -0,0 +1,88 @@
+require 'rails_helper'
+
+describe 'SMS receiving' do
+ include Features::LocalizationHelper
+
+ let(:username) { 'auth_username' }
+ let(:password) { 'auth_password' }
+ let(:access_denied) { 'HTTP Basic: Access denied' }
+ let(:credentials) { "Basic #{Base64.encode64("#{username}:#{password}")}" }
+
+ describe 'HTTP Basic Authentication' do
+ context 'without required credentials' do
+ it 'returns unauthorized status' do
+ post api_sms_receive_path
+
+ expect(response).to have_http_status(:unauthorized)
+ expect(response.body).to include(access_denied)
+ end
+ end
+
+ context 'with required credentials' do
+ it 'returns authorized status' do
+ allow(Figaro.env).to(
+ receive(:twilio_http_basic_auth_username).and_return(username)
+ )
+ allow(Figaro.env).to(
+ receive(:twilio_http_basic_auth_password).and_return(password)
+ )
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(true)
+ )
+
+ post_message
+
+ expect(response).to have_http_status(:accepted)
+ end
+ end
+ end
+
+ describe 'receiving messages' do
+ before do
+ allow(Figaro.env).to(
+ receive(:twilio_http_basic_auth_username).and_return(username)
+ )
+ allow(Figaro.env).to(
+ receive(:twilio_http_basic_auth_password).and_return(password)
+ )
+ end
+
+ context 'when failing' do
+ it 'does not send a reply' do
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(false)
+ )
+
+ expect(SmsReplySenderJob).to_not receive(:perform_later)
+
+ post_message
+
+ expect(response).to have_http_status(:forbidden)
+ end
+ end
+
+ context 'when successful' do
+ it 'sends a reply' do
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(true)
+ )
+
+ expect(SmsReplySenderJob).to receive(:perform_later)
+
+ post_message
+
+ expect(response).to have_http_status(:accepted)
+ end
+ end
+ end
+
+ private
+
+ def post_message
+ post(
+ api_sms_receive_path,
+ params: { Body: 'help' },
+ headers: { 'HTTP_AUTHORIZATION': credentials }
+ )
+ end
+end
diff --git a/spec/services/account_reset_service_spec.rb b/spec/services/account_reset_service_spec.rb
index a9c3edca9d8..6ad96b17490 100644
--- a/spec/services/account_reset_service_spec.rb
+++ b/spec/services/account_reset_service_spec.rb
@@ -8,9 +8,9 @@
let(:user2) { create(:user) }
let(:subject2) { AccountResetService.new(user2) }
- before {
+ before do
allow(Figaro.env).to receive(:account_reset_wait_period_days).and_return('1')
- }
+ end
describe '#create_request' do
it 'creates a new account reset request on the user' do
@@ -37,12 +37,13 @@
describe '#cancel_request' do
it 'removes tokens from a account reset request' do
subject.create_request
- AccountResetService.cancel_request(user.account_reset_request.request_token)
+ cancel = AccountResetService.cancel_request(user.account_reset_request.request_token)
arr = AccountResetRequest.find_by(user_id: user.id)
expect(arr.request_token).to_not be_present
expect(arr.granted_token).to_not be_present
expect(arr.requested_at).to be_present
expect(arr.cancelled_at).to be_present
+ expect(arr).to eq(cancel)
end
it 'does not raise an error for a cancel request with a blank token' do
@@ -103,14 +104,14 @@
subject.create_request
after_waiting_the_full_wait_period do
- notifications_sent = AccountResetService.grant_tokens_and_send_notifications
+ AccountResetService.grant_tokens_and_send_notifications
notifications_sent = AccountResetService.grant_tokens_and_send_notifications
expect(notifications_sent).to eq(0)
end
end
it 'does not send notifications when the request was cancelled' do
- arr = subject.create_request
+ subject.create_request
AccountResetService.cancel_request(AccountResetRequest.all[0].request_token)
after_waiting_the_full_wait_period do
@@ -150,7 +151,7 @@
end
it 'does not send notifications when the request was cancelled' do
- arr = subject.create_request
+ subject.create_request
AccountResetService.cancel_request(AccountResetRequest.all[0].request_token)
notifications_sent = AccountResetService.grant_tokens_and_send_notifications
@@ -160,10 +161,10 @@
end
def after_waiting_the_full_wait_period
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
days = Figaro.env.account_reset_wait_period_days.to_i.days
Timecop.travel(Time.zone.now + days) do
yield
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb
index 6a5ced0f146..601caeeccc2 100644
--- a/spec/services/idv/agent_spec.rb
+++ b/spec/services/idv/agent_spec.rb
@@ -72,7 +72,7 @@
proc { |_, r| r.add_message('bah humbug').add_error(:bad, 'stuff') }
end
Class.new(Proofer::Base) do
- attributes(:foo)
+ required_attributes(:foo)
proof(&logic)
end
end
diff --git a/spec/services/otp_delivery_preference_updater_spec.rb b/spec/services/otp_delivery_preference_updater_spec.rb
new file mode 100644
index 00000000000..d8b28dc88b4
--- /dev/null
+++ b/spec/services/otp_delivery_preference_updater_spec.rb
@@ -0,0 +1,89 @@
+require 'rails_helper'
+
+describe OtpDeliveryPreferenceUpdater do
+ subject do
+ OtpDeliveryPreferenceUpdater.new(
+ user: build_stubbed(:user, otp_delivery_preference: 'sms'),
+ preference: 'sms',
+ context: 'authentication'
+ )
+ end
+
+ describe '#call' do
+ context 'with authentication context' do
+ context 'when otp_delivery_preference is the same as the user otp_delivery_preference' do
+ it 'does not update the user' do
+ expect(UpdateUser).to_not receive(:new)
+
+ subject.call
+ end
+ end
+
+ context 'when otp_delivery_preference is different from the user otp_delivery_preference' do
+ it 'updates the user' do
+ user = build_stubbed(:user, otp_delivery_preference: 'voice')
+ updater = OtpDeliveryPreferenceUpdater.new(
+ user: user,
+ preference: 'sms',
+ context: 'authentication'
+ )
+ attributes = { otp_delivery_preference: 'sms' }
+
+ updated_user = instance_double(UpdateUser)
+ allow(UpdateUser).to receive(:new).
+ with(user: user, attributes: attributes).and_return(updated_user)
+
+ expect(updated_user).to receive(:call)
+
+ updater.call
+ end
+ end
+ end
+
+ context 'with idv context' do
+ context 'when otp_delivery_preference is the same as the user otp_delivery_preference' do
+ it 'does not update the user' do
+ user = build_stubbed(:user, otp_delivery_preference: 'sms')
+ updater = OtpDeliveryPreferenceUpdater.new(
+ user: user,
+ preference: 'sms',
+ context: 'idv'
+ )
+
+ expect(UpdateUser).to_not receive(:new)
+
+ updater.call
+ end
+ end
+
+ context 'when otp_delivery_preference is different from the user otp_delivery_preference' do
+ it 'does not update the user' do
+ user = build_stubbed(:user, otp_delivery_preference: 'voice')
+ updater = OtpDeliveryPreferenceUpdater.new(
+ user: user,
+ preference: 'sms',
+ context: 'idv'
+ )
+
+ expect(UpdateUser).to_not receive(:new)
+
+ updater.call
+ end
+ end
+ end
+
+ context 'when user is nil' do
+ it 'does not update the user' do
+ updater = OtpDeliveryPreferenceUpdater.new(
+ user: nil,
+ preference: 'sms',
+ context: 'idv'
+ )
+
+ expect(UpdateUser).to_not receive(:new)
+
+ updater.call
+ end
+ end
+ end
+end
diff --git a/spec/services/phone_verification_spec.rb b/spec/services/phone_verification_spec.rb
index dbbc692fd1a..86db9f3fe67 100644
--- a/spec/services/phone_verification_spec.rb
+++ b/spec/services/phone_verification_spec.rb
@@ -45,5 +45,21 @@
expect(error).to be_a(PhoneVerification::VerifyError)
end
end
+
+ it 'raises VerifyError when response body is not valid JSON' do
+ PhoneVerification.adapter = FakeAdapter
+ phone = '17035551212'
+ code = '123456'
+
+ allow(FakeAdapter).to receive(:post).and_return(FakeAdapter::EmptyResponse.new)
+
+ expect { PhoneVerification.new(phone: phone, code: code).send_sms }.to raise_error do |error|
+ expect(error.code).to eq 0
+ expect(error.message).to eq ''
+ expect(error.status).to eq 400
+ expect(error.response).to eq ''
+ expect(error).to be_a(PhoneVerification::VerifyError)
+ end
+ end
end
end
diff --git a/spec/services/twilio_service/sms/request_spec.rb b/spec/services/twilio_service/sms/request_spec.rb
new file mode 100644
index 00000000000..ee630086033
--- /dev/null
+++ b/spec/services/twilio_service/sms/request_spec.rb
@@ -0,0 +1,59 @@
+require 'rails_helper'
+
+describe TwilioService::Sms::Request do
+ let(:url) { 'https://example.com' }
+ let(:params) { { Body: 'stop' } }
+ let(:signature) { 'signature' }
+
+ describe '#valid?' do
+ context 'when message signature is invalid' do
+ it 'returns false' do
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(false)
+ )
+
+ message = described_class.new(url, params, signature)
+
+ expect(message.valid?).to be false
+ end
+ end
+
+ context 'when message signature is valid' do
+ before do
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(true)
+ )
+ end
+
+ it 'returns false when message is empty' do
+ empty_params = {}
+
+ message = described_class.new(url, empty_params, signature)
+
+ expect(message.valid?).to be false
+ end
+
+ it 'returns false when message is not a valid message type' do
+ invalid_params = { Body: 'SPORK' }
+
+ message = described_class.new(url, invalid_params, signature)
+
+ expect(message.valid?).to be false
+ end
+
+ it 'returns false when message includes valid message type in sentence' do
+ invalid_params = { Body: 'stop it' }
+
+ message = described_class.new(url, invalid_params, signature)
+
+ expect(message.valid?).to be false
+ end
+
+ it 'returns true when message is valid' do
+ message = described_class.new(url, params, signature)
+
+ expect(message.valid?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/services/twilio_service/sms/response_spec.rb b/spec/services/twilio_service/sms/response_spec.rb
new file mode 100644
index 00000000000..999ac6d8161
--- /dev/null
+++ b/spec/services/twilio_service/sms/response_spec.rb
@@ -0,0 +1,63 @@
+require 'rails_helper'
+
+describe TwilioService::Sms::Response do
+ let(:url) { 'https://example.com' }
+ let(:params) { { Body: 'stop' } }
+ let(:signature) { 'signature' }
+ let(:request) { TwilioService::Sms::Request }
+
+ before do
+ allow_any_instance_of(Twilio::Security::RequestValidator).to(
+ receive(:validate).and_return(true)
+ )
+ end
+
+ describe '#reply' do
+ let(:number) { '+1 202-555-1212' }
+
+ it 'dispatches a message based on its type' do
+ message = request.new(url, params, signature)
+ response = described_class.new(message)
+
+ expect(response).to receive(:stop)
+
+ response.reply
+ end
+
+ it 'is case-insensitive' do
+ message = request.new(url, { Body: 'STOP' }, signature)
+ response = described_class.new(message)
+
+ expect(response).to receive(:stop)
+
+ response.reply
+ end
+
+ it 'calls stop with alternative words' do
+ %w[cancel end quit unsubscribe].each do |stop_word|
+ message = request.new(url, { Body: stop_word }, signature)
+ response = described_class.new(message)
+
+ expect(response).to receive(:stop)
+
+ response.reply
+ end
+ end
+
+ it 'calls stop and returns the appropriate message' do
+ message = request.new(url, { Body: 'stop', From: number }, signature)
+ response = described_class.new(message)
+ expected = { to: number, body: t('sms.stop.message') }
+
+ expect(response.reply).to eq(expected)
+ end
+
+ it 'calls help and returns the appropriate message' do
+ message = request.new(url, { Body: 'help', From: number }, signature)
+ response = described_class.new(message)
+ expected = { to: number, body: t('sms.help.message') }
+
+ expect(response.reply).to eq(expected)
+ end
+ end
+end
diff --git a/spec/services/twilio_service_spec.rb b/spec/services/twilio_service_spec.rb
index 6246839f845..01ea8a591f7 100644
--- a/spec/services/twilio_service_spec.rb
+++ b/spec/services/twilio_service_spec.rb
@@ -1,13 +1,13 @@
require 'rails_helper'
-describe TwilioService do
+describe TwilioService::Utils do
context 'when telephony is disabled' do
before do
expect(FeatureManagement).to receive(:telephony_disabled?).at_least(:once).and_return(true)
end
it 'uses NullTwilioClient' do
- TwilioService.telephony_service = Twilio::REST::Client
+ TwilioService::Utils.telephony_service = Twilio::REST::Client
client = instance_double(NullTwilioClient)
expect(NullTwilioClient).to receive(:new).and_return(client)
@@ -16,11 +16,11 @@
expect(http_client).to receive(:adapter=).with(:typhoeus)
expect(Twilio::REST::Client).to_not receive(:new)
- TwilioService.new
+ TwilioService::Utils.new
end
it 'does not send OTP messages', twilio: true do
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
SmsOtpSenderJob.perform_now(
code: '1234',
@@ -36,7 +36,7 @@
before do
expect(FeatureManagement).to receive(:telephony_disabled?).
at_least(:once).and_return(false)
- TwilioService.telephony_service = Twilio::REST::Client
+ TwilioService::Utils.telephony_service = Twilio::REST::Client
end
it 'uses a real Twilio client' do
@@ -46,20 +46,20 @@
http_client = Struct.new(:adapter)
expect(client).to receive(:http_client).and_return(http_client)
expect(http_client).to receive(:adapter=).with(:typhoeus)
- TwilioService.new
+ TwilioService::Utils.new
end
end
describe '#phone_number' do
it 'randomly samples one of the numbers' do
- expect(TWILIO_NUMBERS).to include(TwilioService.new.phone_number)
+ expect(TWILIO_NUMBERS).to include(TwilioService::Utils.new.phone_number)
end
end
describe '#place_call' do
it 'initiates a phone call with options', twilio: true do
- TwilioService.telephony_service = FakeVoiceCall
- service = TwilioService.new
+ TwilioService::Utils.telephony_service = FakeVoiceCall
+ service = TwilioService::Utils.new
service.place_call(
to: '5555555555',
@@ -74,7 +74,7 @@
end
it 'partially redacts phone numbers embedded in error messages from Twilio' do
- TwilioService.telephony_service = FakeVoiceCall
+ TwilioService::Utils.telephony_service = FakeVoiceCall
raw_message = 'Unable to create record: Account not authorized to call +123456789012.'
error_code = '21215'
status_code = 400
@@ -82,7 +82,7 @@
"Unable to create record: Account " \
"not authorized to call +12345#######.\n\n"
- service = TwilioService.new
+ service = TwilioService::Utils.new
raw_error = Twilio::REST::RestError.new(
raw_message, FakeTwilioErrorResponse.new(error_code)
@@ -100,8 +100,8 @@
it 'sends an SMS with valid attributes', twilio: true do
allow(Figaro.env).to receive(:twilio_messaging_service_sid).and_return('fake_sid')
- TwilioService.telephony_service = FakeSms
- service = TwilioService.new
+ TwilioService::Utils.telephony_service = FakeSms
+ service = TwilioService::Utils.new
service.send_sms(
to: '5555555555',
@@ -118,14 +118,14 @@
end
it 'partially redacts phone numbers embedded in error messages from Twilio' do
- TwilioService.telephony_service = FakeSms
+ TwilioService::Utils.telephony_service = FakeSms
raw_message = "The 'To' number +1 (888) 555-5555 is not a valid phone number"
error_code = '21211'
status_code = 400
sanitized_message = "[HTTP #{status_code}] #{error_code} : The 'To' " \
"number +1 (888) 5##-#### is not a valid phone number\n\n"
- service = TwilioService.new
+ service = TwilioService::Utils.new
raw_error = Twilio::REST::RestError.new(
raw_message, FakeTwilioErrorResponse.new(error_code)
)
diff --git a/spec/support/fake_adapter.rb b/spec/support/fake_adapter.rb
index f721126e560..628551296c9 100644
--- a/spec/support/fake_adapter.rb
+++ b/spec/support/fake_adapter.rb
@@ -20,5 +20,23 @@ def response_body
message: 'Invalid number',
}.to_json
end
+
+ def response_code
+ 400
+ end
+ end
+
+ class EmptyResponse
+ def success?
+ false
+ end
+
+ def response_body
+ ''
+ end
+
+ def response_code
+ 400
+ end
end
end
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index 94e7c921ebf..a7482c619dc 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -10,6 +10,14 @@ def sign_up_with(email)
click_button t('forms.buttons.submit.default')
end
+ def choose_another_security_option(option)
+ click_link t('two_factor_authentication.login_options_link_text')
+
+ expect(current_path).to eq login_two_factor_options_path
+
+ select_2fa_option(option)
+ end
+
def select_2fa_option(option)
find("label[for='two_factor_options_form_selection_#{option}']").click
click_on t('forms.buttons.continue')
@@ -441,11 +449,11 @@ def sign_in_via_branded_page(user)
end
def stub_twilio_service
- twilio_service = instance_double(TwilioService)
+ twilio_service = instance_double(TwilioService::Utils)
allow(twilio_service).to receive(:send_sms)
allow(twilio_service).to receive(:place_call)
- allow(TwilioService).to receive(:new).and_return(twilio_service)
+ allow(TwilioService::Utils).to receive(:new).and_return(twilio_service)
end
def stub_piv_cac_service
diff --git a/spec/support/shared_examples/account_creation.rb b/spec/support/shared_examples/account_creation.rb
index afe342b0246..141d16f4772 100644
--- a/spec/support/shared_examples/account_creation.rb
+++ b/spec/support/shared_examples/account_creation.rb
@@ -79,7 +79,7 @@
expect(page).to have_content t('instructions.account_recovery_setup.piv_cac_next_step')
select_2fa_option('sms')
- click_link t('devise.two_factor_authentication.two_factor_choice_cancel')
+ click_link t('two_factor_authentication.choose_another_option')
expect(page).to have_current_path account_recovery_setup_path
diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb
index 6a89df88c95..0dd1cacce63 100644
--- a/spec/support/shared_examples/sign_in.rb
+++ b/spec/support/shared_examples/sign_in.rb
@@ -35,7 +35,7 @@
visit_idp_from_sp_with_loa1(sp)
click_link t('links.sign_in')
fill_in_credentials_and_submit(user.email, 'Val!d Pass w0rd')
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: old_personal_key)
click_submit_default
@@ -65,7 +65,7 @@
visit_idp_from_sp_with_loa3(sp)
click_link t('links.sign_in')
fill_in_credentials_and_submit(user.email, user.password)
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: personal_key_for_loa3_user(user, pii))
click_submit_default
@@ -90,7 +90,7 @@
visit_idp_from_sp_with_loa1(sp)
trigger_reset_password_and_click_email_link(user.email)
reset_password_and_sign_back_in(user, new_password)
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: old_personal_key)
click_submit_default
@@ -117,7 +117,7 @@
visit_idp_from_sp_with_loa3(sp)
trigger_reset_password_and_click_email_link(user.email)
reset_password_and_sign_back_in(user, new_password)
- click_link t('devise.two_factor_authentication.personal_key_fallback.link')
+ choose_another_security_option('personal_key')
enter_personal_key(personal_key: personal_key_for_loa3_user(user, pii))
click_submit_default
diff --git a/spec/views/account_reset/confirm_delete_account/show.html.slim_spec.rb b/spec/views/account_reset/confirm_delete_account/show.html.slim_spec.rb
index f7f48780e14..795f8092ccd 100644
--- a/spec/views/account_reset/confirm_delete_account/show.html.slim_spec.rb
+++ b/spec/views/account_reset/confirm_delete_account/show.html.slim_spec.rb
@@ -23,10 +23,9 @@
it 'contains link to create a new account' do
render
- puts rendered.inspect
expect(rendered).to have_link(
- t('account_reset.confirm_delete_account.link_text', app: APP_NAME),
- href: root_path
- )
+ t('account_reset.confirm_delete_account.link_text', app: APP_NAME),
+ href: root_path
+ )
end
end
diff --git a/spec/views/account_reset/delete_account/show.html.slim_spec.rb b/spec/views/account_reset/delete_account/show.html.slim_spec.rb
index 6fe0957933e..bd5573e0aba 100644
--- a/spec/views/account_reset/delete_account/show.html.slim_spec.rb
+++ b/spec/views/account_reset/delete_account/show.html.slim_spec.rb
@@ -12,7 +12,6 @@
end
it 'has button to delete' do
-
render
expect(rendered).to have_button t('account_reset.delete_account.delete_button')
end
diff --git a/spec/views/account_reset/request/show.html.slim_spec.rb b/spec/views/account_reset/request/show.html.slim_spec.rb
index d68284b8311..b0fc0d303e8 100644
--- a/spec/views/account_reset/request/show.html.slim_spec.rb
+++ b/spec/views/account_reset/request/show.html.slim_spec.rb
@@ -8,7 +8,6 @@
end
it 'has button to delete' do
-
render
expect(rendered).to have_button t('account_reset.request.yes_continue')
end
diff --git a/spec/views/phone_setup/index.html.slim_spec.rb b/spec/views/phone_setup/index.html.slim_spec.rb
index 94c1c5ebd31..895995f36ec 100644
--- a/spec/views/phone_setup/index.html.slim_spec.rb
+++ b/spec/views/phone_setup/index.html.slim_spec.rb
@@ -17,7 +17,7 @@
it 'renders a link to choose a different option' do
expect(rendered).to have_link(
- t('devise.two_factor_authentication.two_factor_choice_cancel'),
+ t('two_factor_authentication.choose_another_option'),
href: two_factor_options_path
)
end
diff --git a/spec/views/two_factor_authentication/options/index.html.slim_spec.rb b/spec/views/two_factor_authentication/options/index.html.slim_spec.rb
new file mode 100644
index 00000000000..d0aac074d23
--- /dev/null
+++ b/spec/views/two_factor_authentication/options/index.html.slim_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+describe 'two_factor_authentication/options/index.html.slim' do
+ let(:user) { User.new }
+ before do
+ allow(view).to receive(:user_session).and_return({})
+ allow(view).to receive(:current_user).and_return(User.new)
+ @presenter = TwoFactorLoginOptionsPresenter.new(user, view, nil)
+ @two_factor_options_form = TwoFactorLoginOptionsForm.new(user)
+ end
+
+ it 'has a localized title' do
+ expect(view).to receive(:title).with( \
+ t('two_factor_authentication.login_options_title')
+ )
+
+ render
+ end
+
+ it 'has a localized heading' do
+ render
+
+ expect(rendered).to have_content \
+ t('two_factor_authentication.login_options_title')
+ end
+end
diff --git a/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb b/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb
index d09f422d7d7..ce1b436350c 100644
--- a/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb
+++ b/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb
@@ -46,22 +46,9 @@
context 'OTP copy' do
let(:help_text) do
- code_link = link_to(
- t('links.two_factor_authentication.resend_code.sms'),
- otp_send_path(
- locale: LinkLocaleResolver.locale,
- otp_delivery_selection_form: {
- otp_delivery_preference: 'sms',
- resend: true,
- }
- )
- )
-
- t(
- "instructions.mfa.#{presenter_data[:otp_delivery_preference]}.confirm_code_html",
- number: "#{presenter_data[:phone_number]}",
- resend_code_link: code_link
- )
+ t('instructions.mfa.sms.number_message',
+ number: content_tag(:strong, presenter_data[:phone_number]),
+ expiration: Figaro.env.otp_valid_for)
end
it 'informs the user that an OTP has been sent to their number via #help_text' do
@@ -92,8 +79,8 @@
it 'provides an option to use a personal key' do
expect(rendered).to have_link(
- t('devise.two_factor_authentication.personal_key_fallback.link'),
- href: login_two_factor_personal_key_path
+ t('two_factor_authentication.login_options_link_text'),
+ href: login_two_factor_options_path
)
end
end
@@ -178,7 +165,8 @@
render
expect(rendered).to have_link(
- t('links.two_factor_authentication.app'), href: login_two_factor_authenticator_path
+ t('two_factor_authentication.login_options_link_text'),
+ href: login_two_factor_options_path
)
end
end
@@ -203,23 +191,17 @@
})
expect(rendered).to have_link(
- t("links.two_factor_authentication.resend_code.#{otp_delivery_preference}"),
+ t('links.two_factor_authentication.get_another_code'),
href: resend_path
)
end
it 'has a fallback link to send confirmation with voice' do
- expected_fallback_path = otp_send_path(
- otp_delivery_selection_form: { otp_delivery_preference: 'voice' }
- )
- expected_link = link_to(
- t('links.two_factor_authentication.voice'), expected_fallback_path
- )
-
render
- expect(rendered).to include(
- t("instructions.mfa.#{otp_delivery_preference}.fallback_html", link: expected_link)
+ expect(rendered).to have_link(
+ t('two_factor_authentication.login_options_link_text'),
+ href: login_two_factor_options_path
)
end
@@ -265,25 +247,17 @@
)
expect(rendered).to have_link(
- t("links.two_factor_authentication.resend_code.#{otp_delivery_preference}"),
+ t('links.two_factor_authentication.get_another_code'),
href: resend_path
)
end
it 'has a fallback link to send confirmation as SMS' do
- expected_fallback_path = otp_send_path(
- otp_delivery_selection_form: {
- otp_delivery_preference: 'sms',
- }
- )
- expected_link = link_to(
- t('links.two_factor_authentication.sms'), expected_fallback_path
- )
-
render
- expect(rendered).to include(
- t("instructions.mfa.#{otp_delivery_preference}.fallback_html", link: expected_link)
+ expect(rendered).to have_link(
+ t('two_factor_authentication.login_options_link_text'),
+ href: login_two_factor_options_path
)
end
diff --git a/spec/views/two_factor_authentication/personal_key_verification/show.html.slim_spec.rb b/spec/views/two_factor_authentication/personal_key_verification/show.html.slim_spec.rb
index 35d81a637d0..c616b68c0a8 100644
--- a/spec/views/two_factor_authentication/personal_key_verification/show.html.slim_spec.rb
+++ b/spec/views/two_factor_authentication/personal_key_verification/show.html.slim_spec.rb
@@ -4,6 +4,7 @@
let(:user) { build_stubbed(:user, :signed_up) }
before do
+ @presenter = TwoFactorAuthCode::PersonalKeyPresenter.new
@personal_key_form = PersonalKeyForm.new(user)
allow(view).to receive(:current_user).and_return(user)
end
diff --git a/spec/views/two_factor_authentication/totp_verification/show.html.slim_spec.rb b/spec/views/two_factor_authentication/totp_verification/show.html.slim_spec.rb
index adece4b916a..ec5e6509535 100644
--- a/spec/views/two_factor_authentication/totp_verification/show.html.slim_spec.rb
+++ b/spec/views/two_factor_authentication/totp_verification/show.html.slim_spec.rb
@@ -33,19 +33,15 @@
it 'allows the user to fallback to SMS and voice' do
expect(rendered).to have_link(
- t('devise.two_factor_authentication.totp_fallback.sms_link_text'),
- href: otp_send_path(otp_delivery_selection_form: { otp_delivery_preference: 'sms' })
- )
- expect(rendered).to have_link(
- t('devise.two_factor_authentication.totp_fallback.voice_link_text'),
- href: otp_send_path(otp_delivery_selection_form: { otp_delivery_preference: 'voice' })
+ t('two_factor_authentication.login_options_link_text'),
+ href: login_two_factor_options_path
)
end
it 'provides an option to use a personal key' do
expect(rendered).to have_link(
- t('devise.two_factor_authentication.personal_key_fallback.link'),
- href: login_two_factor_personal_key_path
+ t('two_factor_authentication.login_options_link_text'),
+ href: login_two_factor_options_path
)
end
diff --git a/spec/views/users/piv_cac_authentication_setup/new.html.slim_spec.rb b/spec/views/users/piv_cac_authentication_setup/new.html.slim_spec.rb
index 0077477a2d3..11eb4f9f99a 100644
--- a/spec/views/users/piv_cac_authentication_setup/new.html.slim_spec.rb
+++ b/spec/views/users/piv_cac_authentication_setup/new.html.slim_spec.rb
@@ -24,7 +24,7 @@
render
expect(rendered).to have_link(
- t('devise.two_factor_authentication.two_factor_choice_cancel'),
+ t('two_factor_authentication.choose_another_option'),
href: two_factor_options_path
)
end
diff --git a/spec/views/users/totp_setup/new.html.slim_spec.rb b/spec/views/users/totp_setup/new.html.slim_spec.rb
index 84fe4756f0e..db224cb7e5d 100644
--- a/spec/views/users/totp_setup/new.html.slim_spec.rb
+++ b/spec/views/users/totp_setup/new.html.slim_spec.rb
@@ -41,7 +41,7 @@
render
expect(rendered).to have_link(
- t('devise.two_factor_authentication.two_factor_choice_cancel'),
+ t('two_factor_authentication.choose_another_option'),
href: two_factor_options_path
)
end