diff --git a/.reek b/.reek
index f13c3d4d284..d83f1dc79b1 100644
--- a/.reek
+++ b/.reek
@@ -5,6 +5,7 @@ ControlParameter:
- CustomDeviseFailureApp#i18n_message
- OpenidConnectRedirector#initialize
- NoRetryJobs#call
+ - PhoneFormatter#self.format
DuplicateMethodCall:
exclude:
- ApplicationController#disable_caching
@@ -13,7 +14,6 @@ DuplicateMethodCall:
- MfaConfirmationController#handle_invalid_password
- needs_to_confirm_email_change?
- WorkerHealthChecker#status
- - FileEncryptor#encrypt
- UserFlowExporter#self.massage_assets
- BasicAuthUrl#build
- fallback_to_english
@@ -45,6 +45,7 @@ FeatureEnvy:
- Utf8Sanitizer#event_attributes
- Utf8Sanitizer#remote_ip
- Idv::Proofer#validate_vendors
+ - PersonalKeyGenerator#create_legacy_recovery_code
InstanceVariableAssumption:
exclude:
- User
@@ -57,13 +58,13 @@ ManualDispatch:
- CloudhsmKeyGenerator#initialize_settings
NestedIterators:
exclude:
- - FileEncryptor#encrypt
- UserFlowExporter#self.massage_html
- TwilioService#sanitize_phone_number
- ServiceProviderSeeder#run
NilCheck:
enabled: false
LongParameterList:
+ max_params: 4
exclude:
- IdentityLinker#optional_attributes
- Idv::ProoferJob#perform
@@ -92,7 +93,6 @@ TooManyStatements:
- OpenidConnect::AuthorizationController#store_request
- SamlIdpAuthConcern#store_saml_request
- Users::PhoneConfirmationController
- - FileEncryptor#encrypt
- UserFlowExporter#self.massage_assets
- UserFlowExporter#self.massage_html
- UserFlowExporter#self.run
@@ -115,6 +115,8 @@ TooManyMethods:
- Idv::SessionsController
- ServiceProviderSessionDecorator
- SessionDecorator
+ - HolidayService
+ - PhoneDeliveryPresenter
UncommunicativeMethodName:
exclude:
- PhoneConfirmationFlow
@@ -127,6 +129,10 @@ UncommunicativeModuleName:
- X509::Attribute
- X509::Attributes
- X509::SessionStore
+UnusedParameters:
+ exclude:
+ - SmsOtpSenderJob#perform
+ - VoiceOtpSenderJob#perform
UnusedPrivateMethod:
exclude:
- ApplicationController
diff --git a/.rubocop.yml b/.rubocop.yml
index da4f8abaa80..9cb21bca7e5 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -67,6 +67,7 @@ Metrics/ClassLength:
- app/decorators/user_decorator.rb
- app/services/analytics.rb
- app/services/idv/session.rb
+ - app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
Metrics/LineLength:
Description: Limit lines to 100 characters.
diff --git a/Gemfile b/Gemfile
index ba40671bc09..f342eca5aca 100644
--- a/Gemfile
+++ b/Gemfile
@@ -30,7 +30,6 @@ gem 'net-sftp'
gem 'newrelic_rpm'
gem 'pg'
gem 'phonelib'
-gem 'phony_rails'
gem 'pkcs11'
gem 'premailer-rails'
gem 'proofer', github: '18F/identity-proofer-gem', tag: 'v2.5.0'
@@ -62,7 +61,6 @@ gem 'typhoeus'
gem 'uglifier', '~> 3.2'
gem 'valid_email'
gem 'webpacker', '~> 3.4'
-gem 'whenever', require: false
gem 'xml-simple'
gem 'xmlenc', '~> 0.6'
gem 'zxcvbn-js'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7bf3ea4dc70..2aca7ec3c71 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -195,7 +195,6 @@ GEM
chromedriver-helper (1.2.0)
archive-zip (~> 0.10)
nokogiri (~> 1.8)
- chronic (0.10.2)
chunky_png (1.3.8)
codeclimate-engine-rb (0.4.1)
virtus (~> 1.0)
@@ -388,10 +387,6 @@ GEM
ast (~> 2.4.0)
pg (1.0.0)
phonelib (0.6.21)
- phony (2.15.44)
- phony_rails (0.14.6)
- activesupport (>= 3.0)
- phony (> 2.15)
pkcs11 (0.2.7)
powerpack (0.1.1)
premailer (1.11.1)
@@ -582,7 +577,7 @@ GEM
slim (~> 3.0)
sysexits (~> 1.1)
socksify (1.7.1)
- sprockets (3.7.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@@ -651,8 +646,6 @@ GEM
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
- whenever (0.10.0)
- chronic (>= 0.6.3)
xml-simple (1.1.5)
xmldsig (0.6.6)
nokogiri (>= 1.6.8, < 2.0.0)
@@ -723,7 +716,6 @@ DEPENDENCIES
overcommit
pg
phonelib
- phony_rails
pkcs11
premailer-rails
proofer!
@@ -770,7 +762,6 @@ DEPENDENCIES
valid_email
webmock
webpacker (~> 3.4)
- whenever
xml-simple
xmlenc (~> 0.6)
zonebie
@@ -780,4 +771,4 @@ RUBY VERSION
ruby 2.5.1p57
BUNDLED WITH
- 1.16.1
+ 1.16.2
diff --git a/app/assets/images/sp-logos/mycbp.png b/app/assets/images/sp-logos/mycbp.png
new file mode 100644
index 00000000000..fcb8aac4b63
Binary files /dev/null and b/app/assets/images/sp-logos/mycbp.png differ
diff --git a/app/assets/images/sp-logos/usaid.png b/app/assets/images/sp-logos/usaid.png
index be27e210b75..2ee4257e019 100644
Binary files a/app/assets/images/sp-logos/usaid.png and b/app/assets/images/sp-logos/usaid.png differ
diff --git a/app/assets/stylesheets/components/_background.scss b/app/assets/stylesheets/components/_background.scss
index 9c26230e10c..1895ecfd6ef 100644
--- a/app/assets/stylesheets/components/_background.scss
+++ b/app/assets/stylesheets/components/_background.scss
@@ -1,6 +1,7 @@
.bg-gray-lighter { background-color: $gray-lighter; }
.bg-light-blue { background-color: $blue-light; }
.bg-lightest-blue { background-color: $blue-lightest; }
+.bg-lightest-red { background-color: $red-lightest; }
@media #{$breakpoint-sm} {
.sm-bg-light-blue { background-color: $blue-light; }
diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss
index a6f37cb8389..d69531c8002 100644
--- a/app/assets/stylesheets/email.css.scss
+++ b/app/assets/stylesheets/email.css.scss
@@ -38,6 +38,28 @@ h4 {
.button.large.expanded table a { padding: 20px 0; }
+.button.expanded.large .btn-warn-bkg {
+ background-color: $white;
+ border: 0;
+
+ &:hover {
+ background-color: $white;
+ }
+}
+
+.btn-warn-bkg .btn-warn {
+ background-color: $red-lightest;
+ border: 2px solid $red;
+ border-radius: 8px;
+ color: $gray;
+ padding: 10px;
+ width: 50%;
+}
+
+.half {
+ width: 50%;
+}
+
.footer {
background: $secondary-color;
diff --git a/app/assets/stylesheets/variables/_colors.scss b/app/assets/stylesheets/variables/_colors.scss
index f32744a6e76..6fb6022d46e 100644
--- a/app/assets/stylesheets/variables/_colors.scss
+++ b/app/assets/stylesheets/variables/_colors.scss
@@ -22,3 +22,5 @@ $gray-light: #ddd !default;
$gray-lighter: #fafafa !default;
$black: #111 !default;
$pink: #eb4d67 !default;
+$red: #f00 !default;
+$red-lightest: #fff7f8 !default;
diff --git a/app/controllers/account_reset/cancel_controller.rb b/app/controllers/account_reset/cancel_controller.rb
new file mode 100644
index 00000000000..7c12994e22e
--- /dev/null
+++ b/app/controllers/account_reset/cancel_controller.rb
@@ -0,0 +1,25 @@
+module AccountReset
+ class CancelController < ApplicationController
+ def cancel
+ if AccountResetService.cancel_request(params[:token])
+ handle_success
+ else
+ handle_failure
+ end
+ redirect_to root_url
+ end
+
+ private
+
+ def handle_success
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :cancel, token_valid: true)
+ sign_out if current_user
+ flash[:success] = t('devise.two_factor_authentication.account_reset.successful_cancel')
+ end
+
+ def handle_failure
+ return if params[:token].blank?
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :cancel, token_valid: false)
+ end
+ end
+end
diff --git a/app/controllers/account_reset/confirm_delete_account_controller.rb b/app/controllers/account_reset/confirm_delete_account_controller.rb
new file mode 100644
index 00000000000..73f313257f5
--- /dev/null
+++ b/app/controllers/account_reset/confirm_delete_account_controller.rb
@@ -0,0 +1,12 @@
+module AccountReset
+ class ConfirmDeleteAccountController < ApplicationController
+ def show
+ email = flash[:email]
+ if email.blank?
+ redirect_to root_url
+ else
+ render :show, locals: { email: email }
+ end
+ end
+ end
+end
diff --git a/app/controllers/account_reset/confirm_request_controller.rb b/app/controllers/account_reset/confirm_request_controller.rb
new file mode 100644
index 00000000000..d6172b22c0b
--- /dev/null
+++ b/app/controllers/account_reset/confirm_request_controller.rb
@@ -0,0 +1,12 @@
+module AccountReset
+ class ConfirmRequestController < ApplicationController
+ def show
+ email = flash[:email]
+ if email.blank?
+ redirect_to root_url
+ else
+ render :show, locals: { email: email }
+ end
+ end
+ end
+end
diff --git a/app/controllers/account_reset/delete_account_controller.rb b/app/controllers/account_reset/delete_account_controller.rb
new file mode 100644
index 00000000000..b97d9c5a2b3
--- /dev/null
+++ b/app/controllers/account_reset/delete_account_controller.rb
@@ -0,0 +1,48 @@
+module AccountReset
+ class DeleteAccountController < ApplicationController
+ before_action :check_feature_enabled
+ before_action :prevent_parameter_leak, only: :show
+ before_action :check_granted_token
+
+ def show; end
+
+ def delete
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :delete, token_valid: true)
+ email = reset_session_and_set_email
+ UserMailer.account_reset_complete(email).deliver_later
+ redirect_to account_reset_confirm_delete_account_url
+ end
+
+ private
+
+ 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
+ email = user.email
+ user.destroy!
+ sign_out
+ flash[:email] = email
+ end
+
+ def check_granted_token
+ @account_reset_request = AccountResetRequest.from_valid_granted_token(session[:granted_token])
+ return if @account_reset_request
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :delete, token_valid: false)
+ redirect_to root_url
+ end
+
+ def prevent_parameter_leak
+ token = params[:token]
+ return if token.blank?
+ if AccountResetRequest.find_by(granted_token: token)&.granted_token_valid?
+ session[:granted_token] = token
+ redirect_to url_for
+ else
+ redirect_to root_url
+ end
+ end
+ end
+end
diff --git a/app/controllers/account_reset/report_fraud_controller.rb b/app/controllers/account_reset/report_fraud_controller.rb
new file mode 100644
index 00000000000..f3763b29d18
--- /dev/null
+++ b/app/controllers/account_reset/report_fraud_controller.rb
@@ -0,0 +1,25 @@
+module AccountReset
+ class ReportFraudController < ApplicationController
+ def update
+ if AccountResetService.report_fraud(params[:token])
+ handle_success
+ else
+ handle_failure
+ end
+ redirect_to root_url
+ end
+
+ private
+
+ def handle_success
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :fraud, token_valid: true)
+ sign_out if current_user
+ flash[:success] = t('devise.two_factor_authentication.account_reset.successful_cancel')
+ end
+
+ def handle_failure
+ return if params[:token].blank?
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :fraud, token_valid: false)
+ end
+ end
+end
diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb
new file mode 100644
index 00000000000..0b99bc2ace5
--- /dev/null
+++ b/app/controllers/account_reset/request_controller.rb
@@ -0,0 +1,48 @@
+module AccountReset
+ class RequestController < ApplicationController
+ include TwoFactorAuthenticatable
+
+ before_action :check_account_reset_enabled
+ before_action :confirm_two_factor_enabled
+
+ def show; end
+
+ def create
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :request)
+ create_request
+ send_notifications
+ reset_session_with_email
+ redirect_to account_reset_confirm_request_url
+ end
+
+ private
+
+ def check_account_reset_enabled
+ redirect_to root_url unless FeatureManagement.account_reset_enabled?
+ end
+
+ def reset_session_with_email
+ email = current_user.email
+ sign_out
+ flash[:email] = email
+ end
+
+ def send_notifications
+ SmsAccountResetNotifierJob.perform_now(
+ phone: current_user.phone,
+ cancel_token: current_user.account_reset_request.request_token
+ )
+ UserMailer.account_reset_request(current_user).deliver_later
+ end
+
+ def create_request
+ AccountResetService.new(current_user).create_request
+ end
+
+ def confirm_two_factor_enabled
+ return if current_user.two_factor_enabled?
+
+ redirect_to phone_setup_url
+ end
+ end
+end
diff --git a/app/controllers/account_reset/send_notifications_controller.rb b/app/controllers/account_reset/send_notifications_controller.rb
new file mode 100644
index 00000000000..5626674c33e
--- /dev/null
+++ b/app/controllers/account_reset/send_notifications_controller.rb
@@ -0,0 +1,23 @@
+module AccountReset
+ class SendNotificationsController < ApplicationController
+ skip_before_action :verify_authenticity_token
+ before_action :authorize
+
+ def update
+ count = AccountResetService.grant_tokens_and_send_notifications
+ analytics.track_event(Analytics::ACCOUNT_RESET, event: :notifications, count: count)
+ render plain: 'ok'
+ end
+
+ private
+
+ def authorize
+ return if auth_token == Figaro.env.account_reset_auth_token
+ head :unauthorized
+ end
+
+ def auth_token
+ request.headers['X-API-AUTH-TOKEN']
+ end
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index ab0d3cc5335..d852a58b905 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -96,7 +96,11 @@ def redirect_on_timeout
unless current_user
flash[:notice] = t('notices.session_cleared', minutes: Figaro.env.session_timeout_in_minutes)
end
- redirect_to url_for(permitted_timeout_params)
+ begin
+ redirect_to url_for(permitted_timeout_params)
+ rescue ActionController::UrlGenerationError # binary data in params cause redirect to throw this
+ head :bad_request
+ end
end
def permitted_timeout_params
diff --git a/app/controllers/concerns/two_factor_authenticatable.rb b/app/controllers/concerns/two_factor_authenticatable.rb
index a1b64bce45b..e8b7b909dfe 100644
--- a/app/controllers/concerns/two_factor_authenticatable.rb
+++ b/app/controllers/concerns/two_factor_authenticatable.rb
@@ -244,10 +244,15 @@ def phone_view_data
unconfirmed_phone: unconfirmed_phone?,
totp_enabled: current_user.totp_enabled?,
remember_device_available: !idv_context?,
+ account_reset_token: account_reset_token,
}.merge(generic_data)
end
# rubocop:enable MethodLength
+ def account_reset_token
+ current_user&.account_reset_request&.request_token
+ end
+
def authenticator_view_data
{
two_factor_authentication_method: two_factor_authentication_method,
diff --git a/app/controllers/idv/otp_delivery_method_controller.rb b/app/controllers/idv/otp_delivery_method_controller.rb
index edcc275fec4..8978669f80c 100644
--- a/app/controllers/idv/otp_delivery_method_controller.rb
+++ b/app/controllers/idv/otp_delivery_method_controller.rb
@@ -14,7 +14,7 @@ def create
result = @otp_delivery_selection_form.submit(otp_delivery_selection_params)
if result.success?
prompt_to_confirm_phone(
- phone: idv_session.params[:phone],
+ phone: @otp_delivery_selection_form.phone,
context: 'idv',
selected_delivery_method: @otp_delivery_selection_form.otp_delivery_preference
)
diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb
index 74e1554f16c..1e3eef67306 100644
--- a/app/controllers/sign_up/passwords_controller.rb
+++ b/app/controllers/sign_up/passwords_controller.rb
@@ -46,10 +46,12 @@ def permitted_params
def process_successful_password_creation
@user.confirm
+ password = permitted_params[:password]
UpdateUser.new(
user: @user,
- attributes: { reset_requested_at: nil, password: permitted_params[:password] }
+ attributes: { reset_requested_at: nil, password: password }
).call
+ PasswordMetricsIncrementer.new(password).increment_password_metrics
sign_in_and_redirect_user
end
diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb
index 77777f833b1..a73f775d188 100644
--- a/app/controllers/users/reset_passwords_controller.rb
+++ b/app/controllers/users/reset_passwords_controller.rb
@@ -137,10 +137,15 @@ def update_user
attributes = { password: user_params[:password] }
attributes[:confirmed_at] = Time.zone.now unless resource.confirmed?
UpdateUser.new(user: resource, attributes: attributes).call
+ increment_password_metrics
mark_profile_inactive
end
+ def increment_password_metrics
+ PasswordMetricsIncrementer.new(user_params[:password]).increment_password_metrics
+ end
+
def mark_profile_inactive
resource.active_profile&.deactivate(:password_reset)
end
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
index db1a17a40c7..2c93a0fd861 100644
--- a/app/controllers/users/two_factor_authentication_controller.rb
+++ b/app/controllers/users/two_factor_authentication_controller.rb
@@ -5,12 +5,12 @@ class TwoFactorAuthenticationController < ApplicationController
before_action :check_remember_device_preference
def show
- if current_user.totp_enabled?
+ if current_user.piv_cac_enabled?
+ redirect_to login_two_factor_piv_cac_url
+ elsif current_user.totp_enabled?
redirect_to login_two_factor_authenticator_url
elsif current_user.phone_enabled?
validate_otp_delivery_preference_and_send_code
- elsif current_user.piv_cac_enabled?
- redirect_to login_two_factor_piv_cac_url
else
redirect_to two_factor_options_url
end
@@ -25,7 +25,7 @@ def send_code
else
handle_invalid_otp_delivery_preference(result)
end
- rescue Twilio::REST::RestError => exception
+ rescue Twilio::REST::RestError, PhoneVerification::VerifyError => exception
invalid_phone_number(exception)
end
@@ -51,29 +51,21 @@ def handle_invalid_otp_delivery_preference(result)
end
def invalid_phone_number(exception)
+ code = exception.code
analytics.track_event(
- Analytics::TWILIO_PHONE_VALIDATION_FAILED, error: exception.message, code: exception.code
+ Analytics::TWILIO_PHONE_VALIDATION_FAILED, error: exception.message, code: code
)
- flash_error_for_exception(exception)
+ flash[:error] = error_message(code)
redirect_back(fallback_location: account_url)
end
- # rubocop:disable Metrics/MethodLength
- def flash_error_for_exception(exception)
- flash[:error] = case exception.code
- when TwilioService::SMS_ERROR_CODE
- t('errors.messages.invalid_sms_number')
- when TwilioService::INVALID_ERROR_CODE
- t('errors.messages.invalid_phone_number')
- when TwilioService::INVALID_CALLING_AREA_ERROR_CODE
- t('errors.messages.invalid_calling_area')
- when TwilioService::INVALID_VOICE_NUMBER_ERROR_CODE
- t('errors.messages.invalid_voice_number')
- else
- t('errors.messages.otp_failed')
- end
+ def error_message(code)
+ twilio_errors.fetch(code, t('errors.messages.otp_failed'))
+ end
+
+ def twilio_errors
+ TwilioErrors::REST_ERRORS.merge(TwilioErrors::VERIFY_ERRORS)
end
- # rubocop:enable Metrics/MethodLength
def otp_delivery_selection_form
OtpDeliverySelectionForm.new(current_user, phone_to_deliver_to, context)
@@ -105,7 +97,8 @@ def send_user_otp(method)
job = "#{method.capitalize}OtpSenderJob".constantize
job_priority = confirmation_context? ? :perform_now : :perform_later
job.send(job_priority, code: current_user.direct_otp, phone: phone_to_deliver_to,
- otp_created_at: current_user.direct_otp_sent_at.to_s)
+ otp_created_at: current_user.direct_otp_sent_at.to_s,
+ locale: user_locale)
end
def user_selected_otp_delivery_preference
@@ -122,6 +115,11 @@ def phone_to_deliver_to
user_session[:unconfirmed_phone]
end
+ def user_locale
+ available_locales = PhoneVerification::AVAILABLE_LOCALES
+ http_accept_language.language_region_compatible_from(available_locales)
+ end
+
def otp_rate_limiter
@_otp_rate_limited ||= OtpRateLimiter.new(phone: phone_to_deliver_to, user: current_user)
end
diff --git a/app/controllers/usps_upload_controller.rb b/app/controllers/usps_upload_controller.rb
new file mode 100644
index 00000000000..09cd3e1404f
--- /dev/null
+++ b/app/controllers/usps_upload_controller.rb
@@ -0,0 +1,27 @@
+class UspsUploadController < ApplicationController
+ def create
+ authorize do
+ UspsUploader.new.run unless HolidayService.observed_holiday?(today)
+ render plain: 'ok'
+ end
+ end
+
+ private
+
+ def authorize
+ # Check for empty to make sure that the token is configured
+ if authorization_token && authorization_token == Figaro.env.usps_upload_token
+ yield
+ else
+ head :unauthorized
+ end
+ end
+
+ def authorization_token
+ request.headers['X-USPS-UPLOAD-TOKEN']
+ end
+
+ def today
+ Time.zone.today
+ end
+end
diff --git a/app/errors/twilio_errors.rb b/app/errors/twilio_errors.rb
new file mode 100644
index 00000000000..e2f3d37f763
--- /dev/null
+++ b/app/errors/twilio_errors.rb
@@ -0,0 +1,18 @@
+module TwilioErrors
+ REST_ERRORS = {
+ 13_224 => I18n.t('errors.messages.invalid_voice_number'),
+ 21_211 => I18n.t('errors.messages.invalid_phone_number'),
+ 21_215 => I18n.t('errors.messages.invalid_calling_area'),
+ 21_614 => I18n.t('errors.messages.invalid_sms_number'),
+ }.freeze
+
+ VERIFY_ERRORS = {
+ 60_033 => I18n.t('errors.messages.invalid_phone_number'),
+ # invalid country code
+ 60_078 => I18n.t('errors.messages.invalid_phone_number'),
+ # cannot send sms to landline
+ 60_082 => I18n.t('errors.messages.invalid_sms_number'),
+ # phone number not provisioned with any carrier
+ 60_083 => I18n.t('errors.messages.invalid_phone_number'),
+ }.freeze
+end
diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb
index 8d22d368c69..67678c29f25 100644
--- a/app/forms/idv/phone_form.rb
+++ b/app/forms/idv/phone_form.rb
@@ -1,12 +1,11 @@
module Idv
class PhoneForm
include ActiveModel::Model
- include FormPhoneValidator
attr_reader :idv_params, :user, :phone
attr_accessor :international_code
- validate :phone_has_us_country_code
+ validate :phone_is_a_valid_us_number
def initialize(idv_params, user)
@idv_params = idv_params
@@ -16,9 +15,8 @@ def initialize(idv_params, user)
end
def submit(params)
- formatted_phone = PhoneFormatter.new.format(params[:phone])
+ formatted_phone = PhoneFormatter.format(params[:phone])
self.phone = formatted_phone
-
success = valid?
update_idv_params(formatted_phone) if success
@@ -30,16 +28,14 @@ def submit(params)
attr_writer :phone
def initial_phone_value(phone)
- formatted_phone = PhoneFormatter.new.format(
- phone, country_code: PhoneFormatter::DEFAULT_COUNTRY
- )
- return unless Phony.plausible? formatted_phone
+ formatted_phone = PhoneFormatter.format(phone)
+ return unless Phonelib.valid_for_country?(formatted_phone, 'US')
+
self.phone = formatted_phone
end
- def phone_has_us_country_code
- country_code = Phonelib.parse(phone).country_code || '1'
- return if country_code == '1'
+ def phone_is_a_valid_us_number
+ return if Phonelib.valid_for_country?(phone, 'US')
errors.add(:phone, :must_have_us_country_code)
end
@@ -48,8 +44,12 @@ def update_idv_params(phone)
normalized_phone = phone.gsub(/\D/, '')[1..-1]
idv_params[:phone] = normalized_phone
- return idv_params[:phone_confirmed_at] = nil unless phone == user.phone
+ return idv_params[:phone_confirmed_at] = nil unless phone == formatted_user_phone
idv_params[:phone_confirmed_at] = user.phone_confirmed_at
end
+
+ def formatted_user_phone
+ Phonelib.parse(user.phone).international
+ end
end
end
diff --git a/app/forms/otp_delivery_selection_form.rb b/app/forms/otp_delivery_selection_form.rb
index 40cb805ac65..4971439a99f 100644
--- a/app/forms/otp_delivery_selection_form.rb
+++ b/app/forms/otp_delivery_selection_form.rb
@@ -2,14 +2,14 @@ class OtpDeliverySelectionForm
include ActiveModel::Model
include OtpDeliveryPreferenceValidator
- attr_reader :otp_delivery_preference
+ attr_reader :otp_delivery_preference, :phone
validates :otp_delivery_preference, inclusion: { in: %w[sms voice] }
validates :phone, presence: true
def initialize(user, phone_to_deliver_to, context)
@user = user
- @phone = phone_to_deliver_to
+ @phone = PhoneFormatter.format(phone_to_deliver_to)
@context = context
end
@@ -29,7 +29,7 @@ def submit(params)
attr_writer :otp_delivery_preference
attr_accessor :resend
- attr_reader :success, :user, :phone, :context
+ attr_reader :success, :user, :context
def change_otp_delivery_preference_to_sms
user_attributes = { otp_delivery_preference: 'sms' }
diff --git a/app/forms/update_user_password_form.rb b/app/forms/update_user_password_form.rb
index 5d648ff4440..416b7663c72 100644
--- a/app/forms/update_user_password_form.rb
+++ b/app/forms/update_user_password_form.rb
@@ -24,6 +24,7 @@ def process_valid_submission
update_user_password
email_user_about_password_change
encrypt_user_profile_if_active
+ increment_password_metrics
end
def update_user_password
@@ -42,6 +43,10 @@ def encrypt_user_profile_if_active
encryptor.call
end
+ def increment_password_metrics
+ PasswordMetricsIncrementer.new(password).increment_password_metrics
+ end
+
def encryptor
@_encryptor ||= ActiveProfileEncryptor.new(user, user_session, password)
end
diff --git a/app/forms/user_phone_form.rb b/app/forms/user_phone_form.rb
index fca174eebc3..2d8c18f36e2 100644
--- a/app/forms/user_phone_form.rb
+++ b/app/forms/user_phone_form.rb
@@ -27,7 +27,7 @@ def submit(params)
end
def phone_changed?
- user.phone != phone
+ formatted_user_phone != phone
end
private
@@ -43,7 +43,7 @@ def extra_analytics_attributes
def ingest_submitted_params(params)
self.international_code = params[:international_code]
self.submitted_phone = params[:phone]
- self.phone = PhoneFormatter.new.format(
+ self.phone = PhoneFormatter.format(
submitted_phone,
country_code: international_code
)
@@ -61,4 +61,8 @@ def update_otp_delivery_preference_for_user
user_attributes = { otp_delivery_preference: otp_delivery_preference }
UpdateUser.new(user: user, attributes: user_attributes).call
end
+
+ def formatted_user_phone
+ Phonelib.parse(user.phone).international
+ end
end
diff --git a/app/forms/user_piv_cac_setup_form.rb b/app/forms/user_piv_cac_setup_form.rb
index f482b4613de..cad669b7713 100644
--- a/app/forms/user_piv_cac_setup_form.rb
+++ b/app/forms/user_piv_cac_setup_form.rb
@@ -59,7 +59,7 @@ def token_has_correct_nonce
def piv_cac_not_already_associated
self.x509_dn_uuid = @data['uuid']
- self.x509_dn = @data['dn']
+ self.x509_dn = @data['subject']
if User.find_by(x509_dn_uuid: x509_dn_uuid)
self.error_type = 'piv_cac.already_associated'
false
diff --git a/app/forms/user_piv_cac_verification_form.rb b/app/forms/user_piv_cac_verification_form.rb
index afa1ac20fca..bba81df8131 100644
--- a/app/forms/user_piv_cac_verification_form.rb
+++ b/app/forms/user_piv_cac_verification_form.rb
@@ -44,7 +44,7 @@ def not_error_token
false
else
self.x509_dn_uuid = @data['uuid']
- self.x509_dn = @data['dn']
+ self.x509_dn = @data['subject']
true
end
end
diff --git a/app/jobs/sms_account_reset_notifier_job.rb b/app/jobs/sms_account_reset_notifier_job.rb
new file mode 100644
index 00000000000..f4584572f27
--- /dev/null
+++ b/app/jobs/sms_account_reset_notifier_job.rb
@@ -0,0 +1,15 @@
+class SmsAccountResetNotifierJob < ApplicationJob
+ queue_as :sms
+ include Rails.application.routes.url_helpers
+
+ def perform(phone:, cancel_token:)
+ TwilioService.new.send_sms(
+ to: phone,
+ body: I18n.t(
+ 'jobs.sms_account_reset_notifier_job.message',
+ app: APP_NAME,
+ cancel_link: account_reset_cancel_url(token: cancel_token)
+ )
+ )
+ end
+end
diff --git a/app/jobs/sms_otp_sender_job.rb b/app/jobs/sms_otp_sender_job.rb
index b7a959e7f7b..073363f2529 100644
--- a/app/jobs/sms_otp_sender_job.rb
+++ b/app/jobs/sms_otp_sender_job.rb
@@ -1,19 +1,30 @@
class SmsOtpSenderJob < ApplicationJob
queue_as :sms
- def perform(code:, phone:, otp_created_at:)
- send_otp(TwilioService.new, code, phone) if otp_valid?(otp_created_at)
+ # rubocop:disable Lint/UnusedMethodArgument
+ def perform(code:, phone:, otp_created_at:, locale: nil)
+ return unless otp_valid?(otp_created_at)
+
+ if us_number?
+ send_sms_via_twilio_rest_api
+ else
+ send_sms_via_twilio_verify_api(locale)
+ end
end
+ # rubocop:enable Lint/UnusedMethodArgument
private
- def otp_valid?(otp_created_at)
- time_zone = Time.zone
- time_zone.now < time_zone.parse(otp_created_at) + Devise.direct_otp_valid_for
+ def code
+ arguments[0][:code]
end
- def send_otp(twilio_service, code, phone)
- twilio_service.send_sms(
+ def phone
+ arguments[0][:phone]
+ end
+
+ def send_sms_via_twilio_rest_api
+ TwilioService.new.send_sms(
to: phone,
body: I18n.t(
'jobs.sms_otp_sender_job.message',
@@ -22,6 +33,19 @@ def send_otp(twilio_service, code, phone)
)
end
+ 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'
+ end
+
+ def otp_valid?(otp_created_at)
+ time_zone = Time.zone
+ time_zone.now < time_zone.parse(otp_created_at) + Devise.direct_otp_valid_for
+ end
+
def otp_valid_for_minutes
Devise.direct_otp_valid_for.to_i / 60
end
diff --git a/app/jobs/voice_otp_sender_job.rb b/app/jobs/voice_otp_sender_job.rb
index eb451232802..b9d17f696cb 100644
--- a/app/jobs/voice_otp_sender_job.rb
+++ b/app/jobs/voice_otp_sender_job.rb
@@ -4,9 +4,15 @@ class VoiceOtpSenderJob < ApplicationJob
queue_as :voice
- def perform(code:, phone:, otp_created_at:)
+ # rubocop:disable Lint/UnusedMethodArgument
+ # locale is an argument used for the Twilio/Authy Verify service, which uses
+ # a localized message for delivering OTPs via SMS and Voice. As of this
+ # 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)
end
+ # rubocop:enable Lint/UnusedMethodArgument
private
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 330388e7f38..72ff09e8954 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -26,4 +26,20 @@ def account_does_not_exist(email, request_id)
@sign_up_email_url = sign_up_email_url(request_id: request_id, locale: locale_url_param)
mail(to: email, subject: t('user_mailer.account_does_not_exist.subject'))
end
+
+ def account_reset_request(user)
+ account_reset = user.account_reset_request
+ @token = account_reset&.request_token
+ mail(to: user.email, subject: t('user_mailer.account_reset_request.subject'))
+ end
+
+ def account_reset_granted(user, account_reset)
+ @token = account_reset&.request_token
+ @granted_token = account_reset&.granted_token
+ mail(to: user.email, subject: t('user_mailer.account_reset_granted.subject'))
+ end
+
+ def account_reset_complete(email)
+ mail(to: email, subject: t('user_mailer.account_reset_complete.subject'))
+ end
end
diff --git a/app/models/account_reset_request.rb b/app/models/account_reset_request.rb
new file mode 100644
index 00000000000..8d1452f4ff9
--- /dev/null
+++ b/app/models/account_reset_request.rb
@@ -0,0 +1,13 @@
+class AccountResetRequest < ApplicationRecord
+ belongs_to :user
+
+ def self.from_valid_granted_token(granted_token)
+ account_reset = AccountResetRequest.find_by(granted_token: granted_token)
+ account_reset&.granted_token_valid? ? account_reset : nil
+ end
+
+ def granted_token_valid?
+ !granted_token.nil? &&
+ ((Time.zone.now - granted_at) <= Figaro.env.account_reset_token_valid_for_days.to_i.days)
+ end
+end
diff --git a/app/models/concerns/user_access_key_overrides.rb b/app/models/concerns/user_access_key_overrides.rb
index 8b05383ef4b..a9ec587f14b 100644
--- a/app/models/concerns/user_access_key_overrides.rb
+++ b/app/models/concerns/user_access_key_overrides.rb
@@ -17,10 +17,7 @@ def valid_password?(password)
def password=(new_password)
@password = new_password
return if @password.blank?
- digest = Encryption::PasswordVerifier.digest(@password)
- self.encrypted_password_digest = digest.to_s
- # Until we drop the old columns, still write to them so that we can rollback
- write_legacy_password_attributes(digest)
+ self.encrypted_password_digest = Encryption::PasswordVerifier.digest(@password).to_s
end
# This is a devise method, which we are overriding. This should not be removed
@@ -35,13 +32,6 @@ def authenticatable_salt
private
- def write_legacy_password_attributes(digest)
- self.encrypted_password = digest.encrypted_password
- self.encryption_key = digest.encryption_key
- self.password_salt = digest.password_salt
- self.password_cost = digest.password_cost
- end
-
def log_password_verification_failure
metadata = {
event: 'Failure to validate password',
diff --git a/app/models/password_metric.rb b/app/models/password_metric.rb
new file mode 100644
index 00000000000..2b2b496b3b7
--- /dev/null
+++ b/app/models/password_metric.rb
@@ -0,0 +1,28 @@
+class PasswordMetric < ApplicationRecord
+ enum metric: %i[length guesses_log10]
+
+ def self.increment(metric, value)
+ create_row_for_metric_category(metric, value)
+ query = <<-SQL
+ UPDATE password_metrics
+ SET count = count + 1
+ WHERE metric = ? AND value = ?
+ SQL
+ sanitized_query = sanitize_sql_array([query, metrics[metric.to_s], value])
+ connection.execute(sanitized_query)
+ end
+
+ private_class_method def self.create_row_for_metric_category(metric, value)
+ metric_key = metrics[metric.to_s]
+ # Insert a row with the count equal to 0 if a row does not already exist
+ query = <<-SQL
+ INSERT INTO password_metrics (metric, value, count)
+ SELECT ?, ?, 0
+ WHERE NOT EXISTS (
+ SELECT id FROM password_metrics WHERE metric = ? AND value = ?
+ )
+ SQL
+ sanitized_query = sanitize_sql_array([query, metric_key, value, metric_key, value])
+ connection.execute(sanitized_query)
+ end
+end
diff --git a/app/models/service_provider_request.rb b/app/models/service_provider_request.rb
index 8b5e32f218f..e5c1ec60e36 100644
--- a/app/models/service_provider_request.rb
+++ b/app/models/service_provider_request.rb
@@ -1,5 +1,7 @@
class ServiceProviderRequest < ApplicationRecord
def self.from_uuid(uuid)
find_by(uuid: uuid) || NullServiceProviderRequest.new
+ rescue ArgumentError # a null byte in the uuid will raise this
+ NullServiceProviderRequest.new
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5fec213a7a7..3208419e1d5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -35,6 +35,7 @@ class User < ApplicationRecord
has_many :agency_identities, dependent: :destroy
has_many :profiles, dependent: :destroy
has_many :events, dependent: :destroy
+ has_one :account_reset_request, dependent: :destroy
validates :x509_dn_uuid, uniqueness: true, allow_nil: true
diff --git a/app/models/usps_confirmation.rb b/app/models/usps_confirmation.rb
index c232c3cc32d..34f1b5914b8 100644
--- a/app/models/usps_confirmation.rb
+++ b/app/models/usps_confirmation.rb
@@ -1,5 +1,20 @@
class UspsConfirmation < ApplicationRecord
- def decrypted_entry
- UspsConfirmationEntry.new_from_encrypted(entry)
+ # Store the pii as encrypted json
+ def entry=(entry_hash)
+ self[:entry] = encryptor.encrypt(entry_hash.to_json)
+ end
+
+ # Read the pii as a decrypted hash
+ def entry
+ JSON.parse(encryptor.decrypt(self[:entry]), symbolize_names: true)
+ end
+
+ private
+
+ def encryptor
+ # This currently uses the SessionEncryptor, which is meant to be used to
+ # encrypt the session. When this code is changed to integrate a new mail
+ # vendor we should create a purpose built encryptor for that vendor
+ Encryption::Encryptors::SessionEncryptor.new
end
end
diff --git a/app/presenters/idv/otp_delivery_method_presenter.rb b/app/presenters/idv/otp_delivery_method_presenter.rb
index 6d864fa48da..7cdf0f8802a 100644
--- a/app/presenters/idv/otp_delivery_method_presenter.rb
+++ b/app/presenters/idv/otp_delivery_method_presenter.rb
@@ -5,7 +5,7 @@ class OtpDeliveryMethodPresenter
delegate :sms_only?, to: :phone_number_capabilites
def initialize(phone)
- @phone = PhoneFormatter.new.format(phone)
+ @phone = PhoneFormatter.format(phone)
end
def phone_unsupported_message
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 6117d9a4c45..47c4708c4a1 100644
--- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
+++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
@@ -21,6 +21,7 @@ def fallback_links
update_phone_link,
piv_cac_option,
personal_key_link,
+ account_reset_link,
].compact
end
@@ -43,6 +44,7 @@ def cancel_link
:phone_number,
:unconfirmed_phone,
:otp_delivery_preference,
+ :account_reset_token,
:confirmation_for_phone_change,
:voice_otp_delivery_unsupported,
:confirmation_for_idv
@@ -71,6 +73,23 @@ def update_phone_link
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
diff --git a/app/services/account_reset_service.rb b/app/services/account_reset_service.rb
new file mode 100644
index 00000000000..5bb429cb16d
--- /dev/null
+++ b/app/services/account_reset_service.rb
@@ -0,0 +1,83 @@
+class AccountResetService
+ def initialize(user)
+ @user_id = user.id
+ end
+
+ def create_request
+ account_reset = account_reset_request
+ account_reset.update(request_token: SecureRandom.uuid,
+ requested_at: Time.zone.now,
+ cancelled_at: nil,
+ granted_at: nil,
+ granted_token: nil)
+ end
+
+ def self.cancel_request(token)
+ account_reset = token.blank? ? nil : AccountResetRequest.find_by(request_token: token)
+ return false unless account_reset
+ account_reset.update(cancelled_at: Time.zone.now,
+ request_token: nil,
+ granted_token: nil)
+ end
+
+ def self.report_fraud(token)
+ account_reset = token.blank? ? nil : AccountResetRequest.find_by(request_token: token)
+ return false unless account_reset
+ now = Time.zone.now
+ account_reset.update(cancelled_at: now,
+ reported_fraud_at: now,
+ request_token: nil,
+ granted_token: nil)
+ end
+
+ def grant_request
+ token = SecureRandom.uuid
+ arr = AccountResetRequest.find_by(user_id: @user_id)
+ arr.with_lock do
+ return false if arr.granted_token_valid?
+ account_reset_request.update(granted_at: Time.zone.now,
+ granted_token: token)
+ end
+ true
+ end
+
+ def self.grant_tokens_and_send_notifications
+ users_sql = <<~SQL
+ cancelled_at IS NULL AND
+ granted_at IS NULL AND
+ requested_at < :tvalue AND
+ request_token IS NOT NULL AND
+ granted_token IS NULL
+ SQL
+ send_notifications_with_sql(users_sql)
+ end
+
+ def self.send_notifications_with_sql(users_sql)
+ notifications_sent = 0
+ AccountResetRequest.where(
+ users_sql, tvalue: Time.zone.now - Figaro.env.account_reset_wait_period_days.to_i.days
+ ).order('requested_at ASC').each do |arr|
+ notifications_sent += 1 if reset_and_notify(arr)
+ end
+ notifications_sent
+ end
+ private_class_method :send_notifications_with_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
+ private_class_method :reset_and_notify
+
+ private
+
+ def account_reset_request
+ AccountResetRequest.find_or_create_by(user_id: @user_id)
+ end
+end
diff --git a/app/services/analytics.rb b/app/services/analytics.rb
index 4bbd34ba5fe..afca14abc63 100644
--- a/app/services/analytics.rb
+++ b/app/services/analytics.rb
@@ -54,6 +54,7 @@ def browser
end
# rubocop:disable Metrics/LineLength
+ ACCOUNT_RESET = 'Account Reset'.freeze
ACCOUNT_DELETION = 'Account Deletion Requested'.freeze
ACCOUNT_VISIT = 'Account Page Visited'.freeze
EMAIL_AND_PASSWORD_AUTH = 'Email and Password Authentication'.freeze
diff --git a/app/services/encryption/encryptors/attribute_encryptor.rb b/app/services/encryption/encryptors/attribute_encryptor.rb
index dd32f618288..6b5915ecd6a 100644
--- a/app/services/encryption/encryptors/attribute_encryptor.rb
+++ b/app/services/encryption/encryptors/attribute_encryptor.rb
@@ -1,65 +1,71 @@
module Encryption
module Encryptors
class AttributeEncryptor
- def encrypt(plaintext)
- user_access_key = self.class.load_or_init_user_access_key(
- key: current_key, cost: current_cost
- )
- UserAccessKeyEncryptor.new(user_access_key).encrypt(plaintext)
+ include Encodable
+
+ def initialize
+ @aes_cipher = AesCipher.new
+ @stale = false
end
- def decrypt(ciphertext)
- encryption_keys_with_cost.each do |key_with_cost|
- key = key_with_cost.fetch(:key)
- cost = key_with_cost.fetch(:cost)
- result = try_decrypt(ciphertext, key: key, cost: cost)
- return result unless result.nil?
+ def encrypt(plaintext)
+ unless Figaro.env.attribute_encryption_without_kms == 'true'
+ return deprecated_encryptor.encrypt(plaintext)
end
- raise EncryptionError, 'unable to decrypt attribute with any key'
+
+ aes_encrypted_ciphertext = aes_cipher.encrypt(plaintext, current_key)
+ encode(aes_encrypted_ciphertext)
end
- def stale?
- stale
+ def decrypt(ciphertext)
+ return deprecated_encryptor.decrypt(ciphertext) if legacy?(ciphertext)
+ raise EncryptionError, 'ciphertext invalid' unless valid_base64_encoding?(ciphertext)
+ decoded_ciphertext = decode(ciphertext)
+ try_decrypt(decoded_ciphertext)
end
- def self.load_or_init_user_access_key(key:, cost:)
- @_scypt_hashes_by_key ||= {}
- scrypt_hash = @_scypt_hashes_by_key["#{key}:#{cost}"]
- return UserAccessKey.new(scrypt_hash: scrypt_hash) if scrypt_hash.present?
- uak = UserAccessKey.new(password: key, salt: key, cost: cost)
- @_scypt_hashes_by_key["#{key}:#{cost}"] = uak.as_scrypt_hash
- uak
+ def stale?
+ deprecated_encryptor&.stale? || stale
end
private
+ attr_reader :aes_cipher
attr_accessor :stale
- def try_decrypt(ciphertext, key:, cost:)
- user_access_key = self.class.load_or_init_user_access_key(key: key, cost: cost)
- begin
- result = UserAccessKeyEncryptor.new(user_access_key).decrypt(ciphertext)
- self.stale = key != current_key
- result
- rescue EncryptionError
- nil
+ def try_decrypt(decoded_ciphertext)
+ all_keys.each do |key|
+ result = try_decrypt_with_key(decoded_ciphertext, key)
+ return result unless result.nil?
end
+ raise EncryptionError, 'unable to decrypt attribute with any key'
end
- def encryption_keys_with_cost
- @encryption_keys_with_cost ||= [{ key: current_key, cost: current_cost }] + old_keys
+ def try_decrypt_with_key(decoded_ciphertext, key)
+ self.stale = key != current_key
+ aes_cipher.decrypt(decoded_ciphertext, key)
+ rescue EncryptionError
+ nil
+ end
+
+ def legacy?(ciphertext)
+ Encryption::KmsClient.looks_like_kms?(ciphertext) || ciphertext.index('.')
end
def current_key
Figaro.env.attribute_encryption_key
end
- def current_cost
- Figaro.env.attribute_cost
+ def all_keys
+ [current_key].concat(old_keys.collect { |hash| hash['key'] })
end
def old_keys
- JSON.parse(Figaro.env.attribute_encryption_key_queue, symbolize_names: true)
+ JSON.parse(Figaro.env.attribute_encryption_key_queue)
+ end
+
+ def deprecated_encryptor
+ @_deprecated_encryptor ||= DeprecatedAttributeEncryptor.new
end
end
end
diff --git a/app/services/encryption/encryptors/deprecated_attribute_encryptor.rb b/app/services/encryption/encryptors/deprecated_attribute_encryptor.rb
new file mode 100644
index 00000000000..9dda4049f97
--- /dev/null
+++ b/app/services/encryption/encryptors/deprecated_attribute_encryptor.rb
@@ -0,0 +1,66 @@
+module Encryption
+ module Encryptors
+ class DeprecatedAttributeEncryptor
+ def encrypt(plaintext)
+ user_access_key = self.class.load_or_init_user_access_key(
+ key: current_key, cost: current_cost
+ )
+ UserAccessKeyEncryptor.new(user_access_key).encrypt(plaintext)
+ end
+
+ def decrypt(ciphertext)
+ encryption_keys_with_cost.each do |key_with_cost|
+ key = key_with_cost.fetch(:key)
+ cost = key_with_cost.fetch(:cost)
+ result = try_decrypt(ciphertext, key: key, cost: cost)
+ return result unless result.nil?
+ end
+ raise Encryption::EncryptionError, 'unable to decrypt attribute with any key'
+ end
+
+ def stale?
+ stale
+ end
+
+ def self.load_or_init_user_access_key(key:, cost:)
+ @_scypt_hashes_by_key ||= {}
+ scrypt_hash = @_scypt_hashes_by_key["#{key}:#{cost}"]
+ return UserAccessKey.new(scrypt_hash: scrypt_hash) if scrypt_hash.present?
+ uak = UserAccessKey.new(password: key, salt: key, cost: cost)
+ @_scypt_hashes_by_key["#{key}:#{cost}"] = uak.as_scrypt_hash
+ uak
+ end
+
+ private
+
+ attr_accessor :stale
+
+ def try_decrypt(ciphertext, key:, cost:)
+ user_access_key = self.class.load_or_init_user_access_key(key: key, cost: cost)
+ begin
+ result = UserAccessKeyEncryptor.new(user_access_key).decrypt(ciphertext)
+ self.stale = key != current_key
+ result
+ rescue Encryption::EncryptionError
+ nil
+ end
+ end
+
+ def encryption_keys_with_cost
+ @encryption_keys_with_cost ||= [{ key: current_key, cost: current_cost }] + old_keys
+ end
+
+ def current_key
+ Figaro.env.attribute_encryption_key
+ end
+
+ def current_cost
+ Figaro.env.attribute_cost
+ end
+
+ def old_keys
+ JSON.parse(Figaro.env.attribute_encryption_key_queue, symbolize_names: true)
+ end
+ end
+ end
+end
diff --git a/app/services/encryption/kms_client.rb b/app/services/encryption/kms_client.rb
index 56c0c77f8d2..abd27bc6ebb 100644
--- a/app/services/encryption/kms_client.rb
+++ b/app/services/encryption/kms_client.rb
@@ -12,12 +12,20 @@ def encrypt(plaintext)
end
def decrypt(ciphertext)
- return decrypt_kms(ciphertext) if FeatureManagement.use_kms? && looks_like_kms?(ciphertext)
+ return decrypt_kms(ciphertext) if use_kms?(ciphertext)
decrypt_local(ciphertext)
end
+ def self.looks_like_kms?(ciphertext)
+ ciphertext.start_with?(KEY_TYPE[:KMS])
+ end
+
private
+ def use_kms?(ciphertext)
+ FeatureManagement.use_kms? && self.class.looks_like_kms?(ciphertext)
+ end
+
def encrypt_kms(plaintext)
ciphertext_blob = aws_client.encrypt(
key_id: Figaro.env.aws_kms_key_id,
@@ -41,10 +49,6 @@ def decrypt_local(ciphertext)
encryptor.decrypt(ciphertext, Figaro.env.password_pepper)
end
- def looks_like_kms?(ciphertext)
- ciphertext.start_with?(KEY_TYPE[:KMS])
- end
-
def aws_client
@aws_client ||= Aws::KMS::Client.new(
instance_profile_credentials_timeout: 1, # defaults to 1 second
diff --git a/app/services/encryption/password_verifier.rb b/app/services/encryption/password_verifier.rb
index 6e5196bf638..cb8e5974605 100644
--- a/app/services/encryption/password_verifier.rb
+++ b/app/services/encryption/password_verifier.rb
@@ -14,7 +14,7 @@ def self.parse_from_string(digest_string)
data[:password_salt],
data[:password_cost]
)
- rescue JSON::ParserError
+ rescue JSON::ParserError, TypeError
raise EncryptionError, 'digest contains invalid json'
end
@@ -41,12 +41,11 @@ def self.digest(password)
end
def self.verify(password:, digest:)
+ return false if password.blank?
parsed_digest = PasswordDigest.parse_from_string(digest)
- uak = UserAccessKey.new(
- password: password,
- salt: parsed_digest.password_salt,
- cost: parsed_digest.password_cost
- )
+ uak = UserAccessKey.new(password: password,
+ salt: parsed_digest.password_salt,
+ cost: parsed_digest.password_cost)
uak.unlock(parsed_digest.encryption_key)
Devise.secure_compare(uak.encrypted_password, parsed_digest.encrypted_password)
rescue EncryptionError
diff --git a/app/services/file_encryptor.rb b/app/services/file_encryptor.rb
deleted file mode 100644
index 8544e982287..00000000000
--- a/app/services/file_encryptor.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-class FileEncryptor
- class EncryptionError < StandardError; end
-
- def initialize(path_to_public_gpg_key, recipient_email)
- @gpg_key_path = path_to_public_gpg_key
- @recipient_email = recipient_email
- end
-
- def encrypt(text, output_file)
- IO.pipe do |stdin_read_io, stdin_write_io|
- IO.pipe do |stderr_read_io, stderr_write_io|
- stdin_write_io.syswrite(text)
- stdin_write_io.close
-
- success = system(gpg_encrypt_command(output_file), in: stdin_read_io, err: stderr_write_io)
- stderr_write_io.close
- raise EncryptionError, "gpg error: #{stderr_read_io.read}" unless success
- end
- end
- end
-
- def decrypt(passphrase, input_file)
- IO.pipe do |stdout_read_io, stdout_write_io|
- system(
- gpg_decrypt_command(passphrase, input_file),
- out: stdout_write_io,
- err: '/dev/null'
- )
- stdout_write_io.close
- stdout_read_io.read
- end
- end
-
- private
-
- attr_reader :gpg_key_path, :recipient_email
-
- # rubocop:disable MethodLength
- def gpg_encrypt_command(outfile)
- "gpg --no-default-keyring \
- --keyring #{Shellwords.shellescape(gpg_key_path)} \
- --trust-model always \
- --cipher-algo aes256 \
- --digest-algo sha256 \
- --batch \
- --yes \
- --pinentry-mode loopback \
- --status-fd \
- --with-colons \
- --no-tty \
- -e \
- -r #{Shellwords.shellescape(recipient_email)} \
- --output #{Shellwords.shellescape(outfile)}
- "
- end
- # rubocop:enable MethodLength
-
- def gpg_decrypt_command(passphrase, infile)
- password = Shellwords.shellescape(passphrase)
- "echo #{password} | PASSPHRASE=#{password} gpg --batch \
- --pinentry-mode loopback --command-fd 0 -d #{Shellwords.shellescape(infile)}"
- end
-end
diff --git a/app/services/holiday_service.rb b/app/services/holiday_service.rb
new file mode 100644
index 00000000000..6b2b11e940f
--- /dev/null
+++ b/app/services/holiday_service.rb
@@ -0,0 +1,127 @@
+class HolidayService
+ # https://www.opm.gov/policy-data-oversight/snow-dismissal-procedures/federal-holidays
+
+ class << self
+ def holiday?(date)
+ new(date.year).holiday?(date)
+ end
+
+ def observed_holiday?(date)
+ new(date.year).observed_holiday?(date)
+ end
+ end
+
+ attr_reader :year
+
+ def initialize(year)
+ @year = year
+ end
+
+ def holiday?(date)
+ holidays.any? { |holiday| holiday == date }
+ end
+
+ def observed_holiday?(date)
+ observed_holidays.any? { |oh| oh == date }
+ end
+
+ # rubocop:disable Metrics/MethodLength
+ def holidays
+ [
+ new_years,
+ mlk,
+ washington,
+ memorial,
+ independence,
+ labor,
+ columbus,
+ veterans,
+ thanksgiving,
+ christmas,
+ ]
+ end
+ # rubocop:enable Metrics/MethodLength
+
+ def observed_holidays
+ holidays.
+ concat([next_new_years]).
+ map(&method(:observed)).
+ select { |oh| oh.year == year }
+ end
+
+ # January 1st
+ def new_years
+ Date.new(year, 1, 1)
+ end
+
+ # 3rd Monday of January
+ def mlk
+ Date.new(year, 1, 1).
+ step(Date.new(year, 2, 1)).
+ select(&:monday?)[2]
+ end
+
+ # 3rd Monday of February
+ def washington
+ Date.new(year, 2, 1).
+ step(Date.new(year, 3, 1)).
+ select(&:monday?)[2]
+ end
+
+ # Last Monday of May
+ def memorial
+ Date.new(year, 6, 1).
+ step(Date.new(year, 5, 1), -1).
+ find(&:monday?)
+ end
+
+ # July 4th
+ def independence
+ Date.new(year, 7, 4)
+ end
+
+ # First Monday of September
+ def labor
+ Date.new(year, 9, 1).
+ step(Date.new(year, 10, 1)).
+ find(&:monday?)
+ end
+
+ # Second Monday of October
+ def columbus
+ Date.new(year, 10, 1).
+ step(Date.new(year, 11, 1)).
+ select(&:monday?).second
+ end
+
+ # November 11th
+ def veterans
+ Date.new(year, 11, 11)
+ end
+
+ # 4th Thursday of November
+ def thanksgiving
+ Date.new(year, 11, 1).
+ step(Date.new(year, 12, 1)).
+ select(&:thursday?)[3]
+ end
+
+ # December 25th
+ def christmas
+ Date.new(year, 12, 25)
+ end
+
+ # If NY is on a Saturday, the observed holiday would be in the prior year,
+ # make sure to include it in observed holidays when necessary
+ def next_new_years
+ Date.new(year + 1, 1, 1)
+ end
+
+ private
+
+ def observed(date)
+ return date - 1 if date.saturday?
+ return date + 1 if date.sunday?
+ date
+ end
+end
diff --git a/app/services/password_metrics_incrementer.rb b/app/services/password_metrics_incrementer.rb
new file mode 100644
index 00000000000..e95fdfcfac4
--- /dev/null
+++ b/app/services/password_metrics_incrementer.rb
@@ -0,0 +1,19 @@
+class PasswordMetricsIncrementer
+ def initialize(password)
+ @password = password
+ end
+
+ def increment_password_metrics
+ PasswordMetric.increment(:length, password.length)
+ PasswordMetric.increment(:guesses_log10, guesses_log10)
+ end
+
+ private
+
+ attr_reader :password
+
+ # Disable :reek:UncommunicativeMethodName b/c this method name ends with a number
+ def guesses_log10
+ Zxcvbn::Tester.new.test(password).guesses_log10.round(1)
+ end
+end
diff --git a/app/services/personal_key_generator.rb b/app/services/personal_key_generator.rb
index 646a79f2433..1c613fc23ab 100644
--- a/app/services/personal_key_generator.rb
+++ b/app/services/personal_key_generator.rb
@@ -9,23 +9,18 @@ def initialize(user, length: 4)
end
def create
- create_recovery_code
- create_encrypted_recovery_code_digest
+ digest = create_encrypted_recovery_code_digest
+ # Until we drop the old columns, still write to them so that we can rollback
+ create_legacy_recovery_code(digest)
user.save!
raw_personal_key.tr(' ', '-')
end
def verify(plaintext_code)
- @user_access_key = make_user_access_key(normalize(plaintext_code))
- encryption_key, encrypted_code = user.personal_key.split(
- Encryption::Encryptors::AesEncryptor::DELIMITER
+ Encryption::PasswordVerifier.verify(
+ password: normalize(plaintext_code),
+ digest: user.encrypted_recovery_code_digest
)
- begin
- user_access_key.unlock(encryption_key)
- rescue Encryption::EncryptionError => _err
- return false
- end
- Devise.secure_compare(encrypted_code, user_access_key.encrypted_password)
end
def normalize(plaintext_code)
@@ -42,20 +37,19 @@ def normalize(plaintext_code)
attr_reader :user
- def create_recovery_code
- user.recovery_salt = Devise.friendly_token[0, 20]
- user.recovery_cost = Figaro.env.scrypt_cost
- @user_access_key = make_user_access_key(raw_personal_key)
- user.personal_key = hashed_code
+ def create_legacy_recovery_code(digest)
+ user.personal_key = [
+ digest.encryption_key,
+ digest.encrypted_password,
+ ].join(Encryption::Encryptors::AesEncryptor::DELIMITER)
+ user.recovery_salt = digest.password_salt
+ user.recovery_cost = digest.password_cost
end
def create_encrypted_recovery_code_digest
- user.encrypted_recovery_code_digest = {
- encryption_key: user_access_key.encryption_key,
- encrypted_password: user_access_key.encrypted_password,
- password_cost: user.recovery_cost,
- password_salt: user.recovery_salt,
- }.to_json
+ digest = Encryption::PasswordVerifier.digest(raw_personal_key)
+ user.encrypted_recovery_code_digest = digest.to_s
+ digest
end
def encode_code(code:, length:, split:)
@@ -63,22 +57,6 @@ def encode_code(code:, length:, split:)
Base32::Crockford.encode(decoded, length: length, split: split).tr('-', ' ')
end
- def make_user_access_key(code)
- Encryption::UserAccessKey.new(
- password: code,
- salt: user.recovery_salt,
- cost: user.recovery_cost
- )
- end
-
- def hashed_code
- user_access_key.build
- [
- user_access_key.encryption_key,
- user_access_key.encrypted_password,
- ].join(Encryption::Encryptors::AesEncryptor::DELIMITER)
- end
-
def raw_personal_key
@raw_personal_key ||= RandomPhrase.new(num_words: personal_key_length).to_s
end
diff --git a/app/services/phone_formatter.rb b/app/services/phone_formatter.rb
index 50f9a376116..f999a0eaa99 100644
--- a/app/services/phone_formatter.rb
+++ b/app/services/phone_formatter.rb
@@ -1,12 +1,7 @@
-class PhoneFormatter
+module PhoneFormatter
DEFAULT_COUNTRY = 'US'.freeze
- def format(phone, country_code: nil)
- normalized_phone = if country_code
- phone&.phony_normalized(country_code: country_code)
- else
- phone&.phony_normalized(default_country_code: DEFAULT_COUNTRY)
- end
- normalized_phone&.phony_formatted(format: :international, spaces: ' ')
+ def self.format(phone, country_code: nil)
+ Phonelib.parse(phone, country_code || DEFAULT_COUNTRY)&.international
end
end
diff --git a/app/services/phone_number_capabilities.rb b/app/services/phone_number_capabilities.rb
index b19c5f6a651..09fc756b66b 100644
--- a/app/services/phone_number_capabilities.rb
+++ b/app/services/phone_number_capabilities.rb
@@ -50,7 +50,7 @@ def unsupported_location
private
def area_code
- @area_code ||= phone_number_components.second
+ @area_code ||= parsed_phone.area_code
end
def country_code_data
@@ -60,14 +60,10 @@ def country_code_data
end
def international_code
- @international_code ||= phone_number_components.first
+ @international_code ||= parsed_phone.country_code
end
- def phone_number_components
- return [] if phone.blank?
-
- @phone_number_components ||= Phony.split(
- PhonyRails.normalize_number(phone.to_s, default_country_code: :us).slice(1..-1)
- )
+ def parsed_phone
+ @parsed_phone ||= Phonelib.parse(phone)
end
end
diff --git a/app/services/phone_verification.rb b/app/services/phone_verification.rb
new file mode 100644
index 00000000000..94abe87be27
--- /dev/null
+++ b/app/services/phone_verification.rb
@@ -0,0 +1,83 @@
+class PhoneVerification
+ AUTHY_START_ENDPOINT = 'https://api.authy.com/protected/json/phones/verification/start'.freeze
+
+ HEADERS = { 'X-Authy-API-Key' => Figaro.env.twilio_verify_api_key }.freeze
+ OPEN_TIMEOUT = 5
+ READ_TIMEOUT = 5
+
+ AVAILABLE_LOCALES = %w[af ar ca zh zh-CN zh-HK hr cs da nl en fi fr de el he hi hu id it ja ko ms
+ nb pl pt-BR pt ro ru es sv tl th tr vi].freeze
+
+ cattr_accessor :adapter do
+ Typhoeus
+ end
+
+ def initialize(phone:, code:, locale: nil)
+ @phone = phone
+ @code = code
+ @locale = locale
+ end
+
+ def send_sms
+ raise VerifyError.new(code: error_code, message: error_message) unless start_request.success?
+ end
+
+ private
+
+ attr_reader :phone, :code, :locale
+
+ def error_code
+ response_body.fetch('error_code', nil).to_i
+ end
+
+ def error_message
+ response_body.fetch('message', '')
+ end
+
+ def response_body
+ @response_body ||= JSON.parse(start_request.response_body)
+ end
+
+ def start_request
+ @start_request ||= adapter.post(AUTHY_START_ENDPOINT, start_params)
+ end
+
+ # rubocop:disable Metrics/MethodLength
+ def start_params
+ {
+ headers: HEADERS,
+ body: {
+ code_length: 6,
+ country_code: country_code,
+ custom_code: code,
+ locale: locale,
+ phone_number: number_without_country_code,
+ via: 'sms',
+ },
+ connecttimeout: OPEN_TIMEOUT,
+ timeout: READ_TIMEOUT,
+ }
+ end
+ # rubocop:enable Metrics/MethodLength
+
+ def number_without_country_code
+ parsed_phone.raw_national
+ end
+
+ def parsed_phone
+ @parsed_phone ||= Phonelib.parse(phone)
+ end
+
+ def country_code
+ parsed_phone.country_code
+ end
+
+ class VerifyError < StandardError
+ attr_reader :code, :message
+
+ def initialize(code:, message:)
+ @code = code
+ @message = message
+ end
+ end
+end
diff --git a/app/services/twilio_service.rb b/app/services/twilio_service.rb
index 8f84084ea17..53e4e52667c 100644
--- a/app/services/twilio_service.rb
+++ b/app/services/twilio_service.rb
@@ -1,11 +1,6 @@
require 'typhoeus/adapters/faraday'
class TwilioService
- INVALID_VOICE_NUMBER_ERROR_CODE = 13_224
- SMS_ERROR_CODE = 21_614
- INVALID_ERROR_CODE = 21_211
- INVALID_CALLING_AREA_ERROR_CODE = 21_215
-
cattr_accessor :telephony_service do
Twilio::REST::Client
end
diff --git a/app/services/usps_confirmation_entry.rb b/app/services/usps_confirmation_entry.rb
deleted file mode 100644
index 6ebbbb215b3..00000000000
--- a/app/services/usps_confirmation_entry.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-UspsConfirmationEntry = Struct.new(
- :address1,
- :address2,
- :city,
- :first_name,
- :last_name,
- :otp,
- :state,
- :zipcode,
- :issuer
-) do
- def self.encryptor
- # This currently uses the SessionEncryptor, which is meant to be used to
- # encrypt the session. When this code is changed to integrate a new mail
- # vendor we should create a purpose built encryptor for that vendor
- Encryption::Encryptors::SessionEncryptor.new
- end
-
- def self.new_from_hash(hash)
- attrs = new
- hash.each { |key, val| attrs[key] = val }
- attrs
- end
-
- def self.new_from_encrypted(encrypted)
- decrypted = encryptor.decrypt(encrypted)
- new_from_json(decrypted)
- end
-
- def self.new_from_json(pii_json)
- return new if pii_json.blank?
- pii = JSON.parse(pii_json, symbolize_names: true)
- new_from_hash(pii)
- end
-
- def encrypted
- klass = self.class
- klass.encryptor.encrypt(to_json)
- end
-end
diff --git a/app/services/usps_confirmation_maker.rb b/app/services/usps_confirmation_maker.rb
index f8b91cbc1d4..94a8af86722 100644
--- a/app/services/usps_confirmation_maker.rb
+++ b/app/services/usps_confirmation_maker.rb
@@ -10,8 +10,7 @@ def otp
end
def perform
- entry = UspsConfirmationEntry.new_from_hash(attributes)
- UspsConfirmation.create!(entry: entry.encrypted)
+ UspsConfirmation.create!(entry: attributes)
UspsConfirmationCode.create!(
profile: profile,
otp_fingerprint: Pii::Fingerprinter.fingerprint(otp)
diff --git a/app/services/usps_exporter.rb b/app/services/usps_exporter.rb
index 81cd43c2fb3..8ac46be0c3b 100644
--- a/app/services/usps_exporter.rb
+++ b/app/services/usps_exporter.rb
@@ -8,11 +8,10 @@ def initialize(psv_file_path)
end
def run
- psv_buffer = CSV.generate(col_sep: '|', row_sep: "\r\n") do |csv|
+ CSV.open(psv_file_path, 'a', col_sep: '|', row_sep: "\r\n") do |csv|
make_psv(csv)
end
- file_encryptor.encrypt(psv_buffer, psv_file_path)
- clear_entries
+ clear_confirmations
end
private
@@ -20,18 +19,18 @@ def run
attr_reader :psv_file_path
def make_psv(csv)
- csv << make_header_row(entries.size)
- entries.map(&:decrypted_entry).each do |entry|
- csv << make_entry_row(entry)
+ csv << make_header_row(confirmations.size)
+ confirmations.each do |confirmation|
+ csv << make_entry_row(confirmation.entry)
end
end
- def entries
- @entries ||= UspsConfirmation.all
+ def confirmations
+ @confirmations ||= UspsConfirmation.all
end
- def clear_entries
- UspsConfirmation.where(id: entries.map(&:id)).destroy_all
+ def clear_confirmations
+ UspsConfirmation.where(id: confirmations.map(&:id)).destroy_all
end
def make_header_row(num_entries)
@@ -42,17 +41,17 @@ def make_header_row(num_entries)
def make_entry_row(entry)
now = Time.zone.now
due = now + OTP_MAX_VALID_DAYS.days
- service_provider = ServiceProvider.from_issuer(entry.issuer)
+ service_provider = ServiceProvider.from_issuer(entry[:issuer])
[
CONTENT_ROW_ID,
- "#{entry.first_name} #{entry.last_name}",
- entry.address1,
- entry.address2,
- entry.city,
- entry.state,
- entry.zipcode,
- entry.otp,
+ "#{entry[:first_name]} #{entry[:last_name]}",
+ entry[:address1],
+ entry[:address2],
+ entry[:city],
+ entry[:state],
+ entry[:zipcode],
+ entry[:otp],
"#{now.strftime('%-B %-e')}, #{now.year}",
"#{due.strftime('%-B %-e')}, #{due.year}",
service_provider.friendly_name,
@@ -60,11 +59,4 @@ def make_entry_row(entry)
]
end
# rubocop:enable MethodLength, AbcSize
-
- def file_encryptor
- @_file_encryptor ||= FileEncryptor.new(
- Rails.root.join('keys', 'equifax_gpg.pub.bin'),
- Figaro.env.equifax_gpg_email
- )
- end
end
diff --git a/app/services/usps_uploader.rb b/app/services/usps_uploader.rb
index 8b5f394fc9f..70e040f0260 100644
--- a/app/services/usps_uploader.rb
+++ b/app/services/usps_uploader.rb
@@ -11,7 +11,7 @@ def run
def local_path
@_local_path ||= begin
timestamp = Time.zone.now.strftime('%Y%m%d%H%M%S')
- Rails.root.join('tmp', "batch-#{timestamp}.pgp")
+ Rails.root.join('tmp', "batch-#{timestamp}.psv")
end
end
@@ -22,12 +22,10 @@ def build_file
end
def upload_file
- env = Figaro.env
-
Net::SFTP.start(
- env.equifax_sftp_host,
- env.equifax_sftp_username,
- key_data: [RequestKeyManager.equifax_ssh_key.to_pem]
+ env.usps_upload_sftp_host,
+ env.usps_upload_sftp_username,
+ password: env.usps_upload_sftp_password
) do |sftp|
sftp.upload!(local_path.to_s, remote_path)
end
@@ -38,6 +36,10 @@ def clear_file
end
def remote_path
- File.join(Figaro.env.equifax_sftp_directory, 'batch.pgp')
+ File.join(Figaro.env.usps_upload_sftp_directory, 'batch.psv')
+ end
+
+ def env
+ Figaro.env
end
end
diff --git a/app/validators/form_phone_validator.rb b/app/validators/form_phone_validator.rb
index 164b12bfc57..d19b7c13d0f 100644
--- a/app/validators/form_phone_validator.rb
+++ b/app/validators/form_phone_validator.rb
@@ -2,10 +2,12 @@ module FormPhoneValidator
extend ActiveSupport::Concern
included do
- validates_plausible_phone :phone,
- presence: true,
- message: :improbable_phone,
- international_code: ->(form) { form.international_code }
+ validates :phone,
+ presence: true,
+ phone: {
+ message: :improbable_phone,
+ country_specifier: ->(form) { form.international_code },
+ }
validates :international_code, inclusion: {
in: PhoneNumberCapabilities::INTERNATIONAL_CODES.keys,
}
diff --git a/app/view_models/sign_up_completions_show.rb b/app/view_models/sign_up_completions_show.rb
index 48f7989a4fc..1c3ded72ee5 100644
--- a/app/view_models/sign_up_completions_show.rb
+++ b/app/view_models/sign_up_completions_show.rb
@@ -17,6 +17,7 @@ def initialize(loa3_requested:, decorated_session:, current_user:, handoff:)
[[:email], :email],
[[:birthdate], :birthdate],
[[:social_security_number], :social_security_number],
+ [[:x509_subject], :x509_subject],
].freeze
MAX_RECENT_IDENTITIES = 5
diff --git a/app/views/account_reset/confirm_delete_account/show.html.slim b/app/views/account_reset/confirm_delete_account/show.html.slim
new file mode 100644
index 00000000000..5a72212e4bb
--- /dev/null
+++ b/app/views/account_reset/confirm_delete_account/show.html.slim
@@ -0,0 +1,8 @@
+- title t('account_reset.confirm_delete_account.title')
+
+.my2.p3.sm-px4.border.border-red.rounded.rounded-xl.relative
+ = image_tag(asset_url('alert/fail-x.svg'), size: '48x48', alt: '',\
+ class: 'absolute top-n24 left-0 right-0 mx-auto')
+ h3 = t('account_reset.confirm_delete_account.title')
+ p == t('account_reset.confirm_delete_account.info', \
+ email: email, link: link_to(t('account_reset.confirm_delete_account.link_text'), root_path))
diff --git a/app/views/account_reset/confirm_request/show.html.slim b/app/views/account_reset/confirm_request/show.html.slim
new file mode 100644
index 00000000000..7ba5b9833b4
--- /dev/null
+++ b/app/views/account_reset/confirm_request/show.html.slim
@@ -0,0 +1,8 @@
+- title t('account_reset.confirm_request.check_your_email')
+
+.my2.p3.sm-px4.border.border-teal.rounded.rounded-xl.relative
+ = image_tag(asset_url('check-email.svg'), size: '48x48', alt: '',\
+ class: 'absolute top-n24 left-0 right-0 mx-auto')
+ h1.mt1.mb-12p.h3 = t('headings.verify_email')
+ p
+ == t('account_reset.confirm_request.instructions', email: email)
diff --git a/app/views/account_reset/delete_account/show.html.slim b/app/views/account_reset/delete_account/show.html.slim
new file mode 100644
index 00000000000..592457d5d52
--- /dev/null
+++ b/app/views/account_reset/delete_account/show.html.slim
@@ -0,0 +1,15 @@
+- title t('account_reset.delete_account.title')
+
+h1.h3.my0 = t('account_reset.delete_account.title')
+p.mt-tiny.mb0
+ = t('account_reset.delete_account.info')
+br
+h4.my2 = t('account_reset.delete_account.are_you_sure')
+
+= button_to t('account_reset.delete_account.delete_button'), \
+ account_reset_delete_account_path(token: session[:granted_token]), method: :delete, \
+ class: 'btn btn-red col-6 p2 rounded-lg border bw2 bg-lightest-red border-red border-box'
+br
+br
+hr
+= link_to t('account_reset.delete_account.cancel'), root_url
diff --git a/app/views/account_reset/request/show.html.slim b/app/views/account_reset/request/show.html.slim
new file mode 100644
index 00000000000..eb567a0c363
--- /dev/null
+++ b/app/views/account_reset/request/show.html.slim
@@ -0,0 +1,37 @@
+- title t('account_reset.request.title')
+
+h3.my0 = t('account_reset.request.title')
+p.mt-tiny.mb0 == t('account_reset.request.info')
+br
+h4.my0 = t('account_reset.request.personal_key')
+
+p.mt-tiny.mb0
+ = t('account_reset.request.personal_key_info')
+
+hr
+= render 'partials/personal_key/key', code: 'XXXX-XXXX-XXXX-XXXX'
+
+.mb3.right-align
+
+ = link_to t('users.personal_key.print'), '#',
+ data: { print: true },
+ class: 'ml2 btn-border ico ico-print text-decoration-none'
+
+hr
+p.mt-tiny.mb0 == t('account_reset.request.personal_key_trailer', \
+ link: link_to(t('account_reset.request.access_your_account'), login_two_factor_personal_key_url))
+br
+h4.my0 = t('account_reset.request.delete_account')
+p.mt-tiny.mb0 == t('account_reset.request.delete_account_info')
+br
+h5.my2 = t('account_reset.request.are_you_sure')
+= button_to t('account_reset.request.no_cancel'), root_url, method: :get,
+ class: 'btn btn-primary btn-wide mb1 personal-key-continue',
+ 'data-toggle': 'modal'
+= button_to t('account_reset.request.yes_continue'), account_reset_request_path, \
+ class: 'btn btn-link'
+br
+br
+br
+hr
+= link_to t('forms.buttons.back'), root_url
diff --git a/app/views/idv/review/new.html.slim b/app/views/idv/review/new.html.slim
index 8eed040222d..4e21dad468c 100644
--- a/app/views/idv/review/new.html.slim
+++ b/app/views/idv/review/new.html.slim
@@ -7,5 +7,5 @@ h1.h3 = t('idv.titles.session.review')
.mt4
= accordion('review-verified-info', t('idv.messages.review.intro')) do
- phone = @idv_params[:phone]
- - formatted_phone = PhoneFormatter.new.format(phone)
+ - formatted_phone = PhoneFormatter.format(phone)
= render 'shared/pii_review', pii: @idv_params, phone: formatted_phone
diff --git a/app/views/user_mailer/account_reset_complete.html.slim b/app/views/user_mailer/account_reset_complete.html.slim
new file mode 100644
index 00000000000..530ae2ec5f9
--- /dev/null
+++ b/app/views/user_mailer/account_reset_complete.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/user_mailer/account_reset_granted.html.slim b/app/views/user_mailer/account_reset_granted.html.slim
new file mode 100644
index 00000000000..e244672df0a
--- /dev/null
+++ b/app/views/user_mailer/account_reset_granted.html.slim
@@ -0,0 +1,27 @@
+p.lead == t('.intro', app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'))
+table.button.expanded.large
+ tbody
+ tr
+ td
+ table
+ tbody
+ tr
+ td.btn-warn-bkg
+ == link_to t('.button'), account_reset_delete_account_url(token: @granted_token), \
+ target: '_blank', class: 'btn-warn'
+ td.half
+p = link_to account_reset_delete_account_url(token: @granted_token),
+ account_reset_delete_account_url(token: @granted_token), target: '_blank'
+table.spacer
+ tbody
+ tr
+ td.s10 height="10px"
+ |
+table.hr
+ tr
+ th
+ |
+p= t('mailer.confirmation_instructions.footer', confirmation_period: '24 hours')
+p== t('user_mailer.account_reset_granted.help',
+ cancel_account_reset: link_to(t('user_mailer.account_reset_granted.cancel_link_text'),
+ account_reset_cancel_url(token: @token)))
diff --git a/app/views/user_mailer/account_reset_request.html.slim b/app/views/user_mailer/account_reset_request.html.slim
new file mode 100644
index 00000000000..18d8b65d2cc
--- /dev/null
+++ b/app/views/user_mailer/account_reset_request.html.slim
@@ -0,0 +1,18 @@
+p.lead == t('.intro', app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'), \
+ cancel_account_reset: link_to(t('user_mailer.account_reset_granted.cancel_link_text'), \
+ account_reset_cancel_url(token: @token), class: 'blue'))
+
+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/config/application.yml.example b/config/application.yml.example
index 654764aa85b..a4dcd267c5a 100644
--- a/config/application.yml.example
+++ b/config/application.yml.example
@@ -74,11 +74,16 @@ development:
aamva_public_key: '123abc'
aamva_private_key: '123abc'
aamva_verification_url: 'https://example.org:12345/verification/url'
+ account_reset_auth_token: 'abc123'
+ account_reset_enabled: 'true'
+ account_reset_token_valid_for_days: '1'
+ account_reset_wait_period_days: '1'
async_job_refresh_interval_seconds: '5'
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": "old-key-one", "cost": "4000$8$4$" }, { "key": "old-key-one", "cost": "4000$8$4$" }]'
+ attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111", "cost": "4000$8$4$" }, { "key": "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'
aws_region: 'us-east-1'
@@ -119,7 +124,7 @@ development:
equifax_ssh_passphrase: 'sekret'
exception_recipients: 'test1@test.com'
hmac_fingerprinter_key: 'a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c'
- hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]'
+ hmac_fingerprinter_key_queue: '["11111111111111111111111111111111", "22222222222222222222222222222222"]'
identity_pki_disabled: 'true'
issuers_with_email_nameid_format: ''
lexisnexis_account_id: 'test_account'
@@ -185,6 +190,11 @@ development:
usps_confirmation_max_days: '10'
enable_i18n_mode: 'false'
enable_load_testing_mode: 'false'
+ usps_upload_sftp_directory: '/directory'
+ usps_upload_sftp_host: 'example.com'
+ usps_upload_sftp_username: 'user'
+ usps_upload_sftp_password: 'pass'
+ usps_upload_token: '123ABC'
# These values serve as defaults for all production-like environments, which
# includes *.identitysandbox.gov and *.login.gov.
@@ -198,11 +208,16 @@ production:
aamva_public_key: # Base64 encoded public key for AAMVA
aamva_private_key: # Base64 encoded private key for AAMVA
aamva_verification_url: # DLDV Verification URL
+ account_reset_auth_token:
+ account_reset_enabled: 'true'
+ account_reset_token_valid_for_days: '1'
+ account_reset_wait_period_days: '1'
async_job_refresh_interval_seconds: '5'
async_job_refresh_max_wait_seconds: '15'
attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5)
attribute_encryption_key: # generate via `rake secret`
attribute_encryption_key_queue: # '[{ "key": "old-key-one", "cost": "4000$8$4$" }, { "key": "old-key-one", "cost": "4000$8$4$" }]'
+ attribute_encryption_without_kms: 'false'
available_locales: 'en es fr'
aws_kms_key_id:
aws_region:
@@ -291,21 +306,32 @@ production:
twilio_auth_token: # Twilio auth token
twilio_messaging_service_sid: # Twilio CoPilot SID
twilio_record_voice: 'false'
+ twilio_verify_api_key: 'change-me'
use_kms: 'true'
usps_confirmation_max_days: '30'
enable_i18n_mode: 'false'
enable_load_testing_mode: 'false'
+ usps_upload_sftp_directory:
+ usps_upload_sftp_host:
+ usps_upload_sftp_username:
+ usps_upload_sftp_password:
+ usps_upload_token:
test:
aamva_cert_enabled: 'true'
aamva_public_key: '123abc'
aamva_private_key: '123abc'
aamva_verification_url: 'https://example.org:12345/verification/url'
+ account_reset_auth_token: 'test'
+ account_reset_enabled: 'true'
+ account_reset_token_valid_for_days: '1'
+ account_reset_wait_period_days: '1'
async_job_refresh_interval_seconds: '1'
async_job_refresh_max_wait_seconds: '15'
attribute_cost: '800$8$1$' # SCrypt::Engine.calibrate(max_time: 0.01)
attribute_encryption_key: '2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25'
- attribute_encryption_key_queue: '[{ "key": "old-key-one", "cost": "4000$8$4$" }, { "key": "old-key-one", "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-test-keymaker'
aws_region: 'us-east-1'
@@ -401,7 +427,13 @@ test:
twilio_auth_token: 'token1'
twilio_messaging_service_sid: '123abc'
twilio_record_voice: 'true'
+ twilio_verify_api_key: 'secret'
use_kms: 'false'
usps_confirmation_max_days: '10'
enable_i18n_mode: 'false'
enable_load_testing_mode: 'false'
+ usps_upload_sftp_directory: '/directory'
+ usps_upload_sftp_host: 'example.com'
+ usps_upload_sftp_username: 'user'
+ usps_upload_sftp_password: 'pass'
+ usps_upload_token: 'test_token'
diff --git a/config/initializers/figaro.rb b/config/initializers/figaro.rb
index c9b382e94e4..ed1d5602f6d 100644
--- a/config/initializers/figaro.rb
+++ b/config/initializers/figaro.rb
@@ -31,7 +31,6 @@
'password_pepper',
'password_strength_enabled',
'queue_health_check_dead_interval_seconds',
- 'queue_health_check_frequency_seconds',
'reauthn_window',
'recovery_code_length',
'redis_url',
diff --git a/config/locales/account_reset/en.yml b/config/locales/account_reset/en.yml
new file mode 100644
index 00000000000..fe5a2b35f08
--- /dev/null
+++ b/config/locales/account_reset/en.yml
@@ -0,0 +1,48 @@
+---
+en:
+ account_reset:
+ request:
+ title: Account deletion and reset
+ info: If you can't access your account through the security options you set
+ up previously, deleting your account and creating a new one is the only option.
+
We can't undo an account delete, so please make sure you don't have
+ another security option you can use instead like your personal key.
+ personal_key: Do you have your personal key?
+ personal_key_info: Your personal key is a 16 character code that was given to
+ you at account creation as a recovery method--see example below.
+ personal_key_trailer: If you have your personal key, you can use it to %{link}
+ instead of resetting.
+ access_your_account: access your account
+ delete_account: Delete your account
+ delete_account_info: Deleting your existing account and creating a new one will
+ allow you to use the same email address and set up new security options. However,
+ deleting will remove any agency applications you have linked to your account
+ 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.
+ 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
+ yes_continue: Yes, continue deletion.
+ confirm_request:
+ check_your_email: Check your email
+ instructions: We sent an email to %{email} to begin the account
+ delete process. Follow the instructions in your email to complete the process.
+
As a security measure, we also sent a text to your registered phone
+ number.
You can close this window if you are done.
+ delete_account:
+ title: Deleting your account should be your last resort
+ info: Deleting your account should be your last resort if you are locked out
+ of your account. You will not be able to recover any information linked to
+ your account. Once your account is deleted, you can create a new one using
+ the same email address.
+ are_you_sure: Are you sure you want to delete your account?
+ delete_button: Delete account
+ cancel: Cancel
+ confirm_delete_account:
+ title: You have deleted your account
+ info: The account for %{email} has been deleted. We sent an
+ email confirmation of the account deletion.
You may %{link} or close
+ this window if you're done.
+ link_text: create a new account
diff --git a/config/locales/account_reset/es.yml b/config/locales/account_reset/es.yml
new file mode 100644
index 00000000000..a069a46a609
--- /dev/null
+++ b/config/locales/account_reset/es.yml
@@ -0,0 +1,52 @@
+---
+es:
+ account_reset:
+ request:
+ title: Eliminación y restablecimiento de cuenta
+ info: Si no puede acceder a su cuenta a través de las opciones de seguridad
+ que configuró anteriormente, eliminar la cuenta y crear una nueva es la única
+ opción.
No podemos deshacer la eliminación de una cuenta, así que
+ por favor asegúrese de no tener otra opción de seguridad que puede usar como
+ su clave personal.
+ personal_key: "¿Tienes tu clave personal?"
+ personal_key_info: Su clave personal es un código de 16 caracteres que se le
+ dio a en la creación de la cuenta como método de recuperación; consulte el
+ ejemplo a continuación.
+ personal_key_trailer: Si tiene su clave personal, puede usarla en %{link}
+ en lugar de restablecer.
+ access_your_account: acceder a tu cuenta
+ delete_account: Eliminar su cuenta
+ delete_account_info: Eliminar su cuenta existente y crear una nueva
+ le permite usar la misma dirección de correo electrónico y configurar nuevas
+ opciones de seguridad. Sin embargo, eliminar eliminará cualquier aplicación
+ de agencia que haya vinculado a su cuenta y deberá restaurar cada conexión.
+
Si continúas, tú primero recibirá una confirmación por correo electrónico.
+ Como medida de seguridad, lo hará reciba otro correo electrónico con el enlace
+ para seguir eliminando su cuenta las 24 horas después del correo electrónico
+ de confirmación inicial llega.
+ are_you_sure: "¿Estás seguro de que no tienes acceso a ninguno de tus métodos
+ de seguridad? O tu clave personal?"
+ no_cancel: No, cancelar
+ yes_continue: Sí, continúa la eliminación.
+ confirm_request:
+ check_your_email: Consultar su correo electrónico
+ instructions: Enviamos un correo electrónico a %{email} para
+ comenzar el proceso de eliminación de cuenta. Siga las instrucciones en su
+ correo electrónico para completar el proceso.
Como medida de seguridad,
+ también enviamos un mensaje de texto a su registro número de teléfono.
+ Puede cerrar esta ventana si ha terminado.
+ delete_account:
+ title: Eliminar tu cuenta debería ser tu último recurso
+ info: Eliminar su cuenta debe ser su último recurso si está bloqueado
+ de tu cuenta No podrá recuperar ninguna información vinculada a su cuenta.
+ Una vez que se elimine su cuenta, puede crear una nueva usando la misma dirección
+ de correo electrónico.
+ are_you_sure: "¿Seguro que quieres eliminar tu cuenta?"
+ delete_button: Borrar cuenta
+ cancel: Cancelar
+ confirm_delete_account:
+ title: Has eliminado tu cuenta
+ info: La cuenta para %{email} ha sido eliminada. Nosotros enviamos
+ una confirmación por correo electrónico de la eliminación de la cuenta.
+ Puede %{link} o cierra esta ventana si ya terminaste.
+ link_text: crea una cuenta nueva
diff --git a/config/locales/account_reset/fr.yml b/config/locales/account_reset/fr.yml
new file mode 100644
index 00000000000..fc837132905
--- /dev/null
+++ b/config/locales/account_reset/fr.yml
@@ -0,0 +1,53 @@
+---
+fr:
+ account_reset:
+ request:
+ title: Suppression de compte et réinitialisation
+ info: Si vous ne pouvez pas accéder à votre compte via les options de sécurité
+ que vous avez définies auparavant, la suppression de votre compte et la création
+ d'un nouveau compte est la seule option.
Nous ne pouvons pas annuler
+ un compte supprimer, alors s'il vous plaît assurez-vous que vous n'avez pas
+ une autre option de sécurité que vous pouvez utiliser à la place comme votre
+ clé personnelle.
+ personal_key: Avez-vous votre clé personnelle?
+ personal_key_info: Votre clé personnelle est un code de 16 caractères qui a
+ été donné à vous à la création de compte comme une méthode de récupération
+ - voir l'exemple ci-dessous.
+ personal_key_trailer: Si vous avez votre clé personnelle, vous pouvez l'utiliser
+ pour %{link} au lieu de réinitialiser.
+ access_your_account: accéder à votre compte
+ delete_account: Supprimer votre compte
+ delete_account_info: Supprimer votre compte existant et en créer un nouveau
+ vous permet d'utiliser la même adresse e-mail et de définir de nouvelles options
+ de sécurité. cependant, la suppression supprimera toutes les applications
+ d'agence que vous avez liées à votre compte et vous devrez restaurer chaque
+ connexion.
Si vous continuez, vous recevra d'abord un email de confirmation.
+ Par mesure de sécurité, vous devrez recevoir un autre e-mail avec le lien
+ pour continuer la suppression de votre compte 24 heures après l'email de confirmation
+ initial arrive.
+ are_you_sure: Êtes-vous sûr de n'avoir accès à aucune de vos méthodes de sécurité?
+ OU votre clé personnelle?
+ no_cancel: Non, annuler
+ yes_continue: Oui, continuez la suppression.
+ confirm_request:
+ check_your_email: Vérifiez votre email
+ instructions: Nous avons envoyé un e-mail à %{email} pour commencer
+ le compte. Supprimer le processus. Suivez les instructions dans votre e-mail
+ pour terminer le processus.
Par mesure de sécurité, nous avons également
+ envoyé un SMS sur votre téléphone enregistré nombre.
Vous pouvez
+ fermer cette fenêtre si vous avez terminé.
+ delete_account:
+ title: La suppression de votre compte devrait être votre dernier recours
+ info: La suppression de votre compte devrait être votre dernier recours si vous
+ êtes en lock-out de votre compte Vous ne pourrez pas récupérer les informations
+ liées à ton compte. Une fois votre compte supprimé, vous pouvez en créer un
+ nouveau en utilisant la même adresse e-mail.
+ are_you_sure: Êtes-vous sûr de vouloir supprimer votre compte?
+ delete_button: Supprimer le compte
+ cancel: Annuler
+ confirm_delete_account:
+ title: Vous avez supprimé votre compte
+ info: Le compte pour %{email} a été supprimé. Nous avons envoyé
+ un email de confirmation de la suppression du compte.
Vous pouvez
+ %{link} ou fermer cette fenêtre si vous avez terminé.
+ link_text: créer un nouveau compte
diff --git a/config/locales/devise/en.yml b/config/locales/devise/en.yml
index 85e3df9b9c8..e9602f0374c 100644
--- a/config/locales/devise/en.yml
+++ b/config/locales/devise/en.yml
@@ -131,8 +131,8 @@ en:
link: Use a personal key instead
text_html: Don’t have access to your phone? %{link}.
personal_key_header_text: Enter your personal key
- personal_key_prompt: You can use this personal key once. If you still need a
- code after signing in, go to your account settings page to get a new one.
+ personal_key_prompt: You can use this personal key once. After you enter it,
+ you'll be provided a new key.
phone_sms_label: Mobile phone number
phone_sms_info_html: We'll text a security code each time you sign in.
phone_voice_label: Phone number
@@ -145,6 +145,16 @@ en:
please_confirm: Your phone number has been set. Confirm it by entering the security
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
+ has been cancelled.
+ link: deleting your account
+ text_html: If you can't use any of these security options above, you can reset
+ your preferences by %{link}.
+ cancel_link: Cancel your request
+ pending_html: You currently have a pending request to delete your account.
+ It takes 24 hours from the time you made the request to complete the process.
+ Please check back later. %{cancel_link}
read_about_two_factor_authentication:
link: read about two-factor authentication
text_html: You can %{link} and why we use it at our Help page.
@@ -169,6 +179,22 @@ en:
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 008ce5c4413..1e273df55ee 100644
--- a/config/locales/devise/es.yml
+++ b/config/locales/devise/es.yml
@@ -135,9 +135,8 @@ es:
link: Use una clave personal en su lugar
text_html: "¿No tiene acceso a su teléfono? %{link}."
personal_key_header_text: Ingrese su clave personal
- personal_key_prompt: Puede usar esta clave personal una vez. Si todavía necesita
- un código después de iniciar una sesión, vaya a la página de configuración
- de su cuenta para obtener una clave nueva.
+ personal_key_prompt: Puede usar esta clave personal una vez. Después de ingresarlo,
+ se le dará una nueva clave.
phone_sms_label: Número de teléfono móvil
phone_sms_info_html: Le enviaremos un mensaje de texto con un código de seguridad
cada vez que inicie sesión.
@@ -151,6 +150,16 @@ es:
please_confirm: Su número de teléfono ha sido establecido. Confírmelo ingresando
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.
+ link: eliminando su cuenta
+ text_html: Si no puede usar ninguna de estas opciones de seguridad anteriores,
+ puede restablecer tus preferencias por %{link}.
+ cancel_link: Cancelar su solicitud
+ pending_html: Actualmente tiene una solicitud pendiente para eliminar su cuenta.
+ Se necesitan 24 horas desde el momento en que realizó la solicitud para
+ completar el proceso. Por favor, vuelva más tarde. %{cancel_link}
read_about_two_factor_authentication:
link: leer acerca de la autenticación de dos factores
text_html: Puede %{link} y por qué la utilizamos en nuestra página de Ayuda.
@@ -176,6 +185,23 @@ es:
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 fefc7cd17b1..f9f764116c5 100644
--- a/config/locales/devise/fr.yml
+++ b/config/locales/devise/fr.yml
@@ -143,8 +143,7 @@ fr:
text_html: Vous n'avez pas accès à votre téléphone? %{link}.
personal_key_header_text: Entrez votre clé personnelle
personal_key_prompt: Vous pouvez utiliser cette clé personnelle une fois seulement.
- Si vous avez toujours besoin d'un code après votre connexion, allez à la page
- des réglages de votre compte pour en obtenir un nouveau.
+ Une fois que vous l'entrez, vous recevrez une nouvelle clé.
phone_sms_label: Numéro de téléphone portable
phone_sms_info_html: Nous vous enverrons un code de sécurité chaque
fois que vous vous connectez.
@@ -158,6 +157,17 @@ fr:
please_confirm: Votre numéro de téléphone a été entré. Confirmez-le en entrant
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é.
+ 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}.
+ cancel_link: Annuler votre demande
+ pending_html: Vous avez actuellement une demande en attente pour supprimer
+ votre compte. Il faut compter 24 heures à partir du moment où vous avez
+ fait la demande pour terminer le processus. Veuillez vérifier plus tard.
+ %{cancel_link}
read_about_two_factor_authentication:
link: lire sur l'authentification à deux facteurs
text_html: Vous pouvez %{link} et pourquoi nous l'utilisons sur notre page
@@ -184,6 +194,23 @@ fr:
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/help_text/en.yml b/config/locales/help_text/en.yml
index 8e53129fb84..5943d76e5b0 100644
--- a/config/locales/help_text/en.yml
+++ b/config/locales/help_text/en.yml
@@ -11,6 +11,7 @@ en:
intro_html: 'This is the only information %{app_name} will share with %{sp}:'
phone: Phone number
social_security_number: Social Security number
+ x509_subject: PIV/CAC Identity
no_factor:
delete_account: To delete your account, please confirm your password and security
code.
diff --git a/config/locales/help_text/es.yml b/config/locales/help_text/es.yml
index 35efc5fde0b..cfc2b3207af 100644
--- a/config/locales/help_text/es.yml
+++ b/config/locales/help_text/es.yml
@@ -11,6 +11,7 @@ es:
intro_html: 'Esta es la única información que %{app_name} compartirá con %{sp}:'
phone: Teléfono
social_security_number: Número de Seguro Social
+ x509_subject: NOT TRANSLATED YET
no_factor:
delete_account: Para eliminar su cuenta, confirme su contraseña y código de
seguridad.
diff --git a/config/locales/help_text/fr.yml b/config/locales/help_text/fr.yml
index 49526b9880f..2ddba3f11bb 100644
--- a/config/locales/help_text/fr.yml
+++ b/config/locales/help_text/fr.yml
@@ -12,6 +12,7 @@ fr:
%{sp}:'
phone: Numéro de téléphone
social_security_number: Numéro de sécurité sociale
+ x509_subject: NOT TRANSLATED YET
no_factor:
delete_account: Pour supprimer votre compte, veuillez confirmer votre mot de
passe et votre code de sécurité.
diff --git a/config/locales/jobs/en.yml b/config/locales/jobs/en.yml
index d1394268e98..d7d47937775 100644
--- a/config/locales/jobs/en.yml
+++ b/config/locales/jobs/en.yml
@@ -4,3 +4,7 @@ en:
sms_otp_sender_job:
message: "%{code} is your %{app} one-time security code. This code will expire
in %{expiration} minutes."
+ 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
+ cancel: %{cancel_link}'
diff --git a/config/locales/jobs/es.yml b/config/locales/jobs/es.yml
index 4a8b6de4cd5..e0f5c767b71 100644
--- a/config/locales/jobs/es.yml
+++ b/config/locales/jobs/es.yml
@@ -4,3 +4,7 @@ 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_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:
+ %{cancel_link}'
diff --git a/config/locales/jobs/fr.yml b/config/locales/jobs/fr.yml
index 51a68e33671..76c1a828e42 100644
--- a/config/locales/jobs/fr.yml
+++ b/config/locales/jobs/fr.yml
@@ -4,3 +4,7 @@ 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_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,
+ veuillez cancel: %{cancel_link}'
diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml
index 1acb8d977cc..0ba57faca25 100644
--- a/config/locales/user_mailer/en.yml
+++ b/config/locales/user_mailer/en.yml
@@ -8,6 +8,27 @@ 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_request:
+ help: ''
+ intro: 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, %{cancel_account_reset}.
+ subject: Delete your account
+ account_reset_granted:
+ cancel_link_text: please cancel
+ help: If you don’t want to delete your account, %{cancel_account_reset}.
+ intro: You are receiving this email because you requested to delete and reset
+ your login.gov account.
Deleting your account should be your last
+ resort if you are locked out of your account. You will not be able to recover
+ any information linked to your account. Once your account is deleted, you
+ can create a new one using the same email address.
Are you sure you
+ want to delete your account?
+ button: Yes, continue deleting
+ subject: Delete your account
+ account_reset_complete:
+ intro: This email confirms you have deleted your login.gov account.
+ subject: Account deleted
+ help: ''
contact_link_text: contact us
email_changed:
help: If you did not want to change your email address, please visit the %{app}
diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml
index 09b08534852..0c4d9b723a7 100644
--- a/config/locales/user_mailer/es.yml
+++ b/config/locales/user_mailer/es.yml
@@ -5,6 +5,27 @@ es:
intro: NOT TRANSLATED YET
link_text: NOT TRANSLATED YET
subject: NOT TRANSLATED YET
+ account_reset_request:
+ help: ''
+ intro: Has solicitado eliminar tu cuenta de login.gov.
Su la
+ solicitud se procesará en 24 horas.
Si no quieres para eliminar
+ su cuenta, %{cancel_account_reset}.
+ subject: Eliminar su cuenta
+ account_reset_granted:
+ cancel_link_text: por favor cancele
+ help: Si no desea eliminar su cuenta, %{cancel_account_reset}.
+ intro: Recibes este correo electrónico porque solicitaste eliminar y restablecer
+ su cuenta de login.gov
Eliminar tu cuenta debería ser tu último recurso
+ si está bloqueado de su cuenta. No podrás recuperar cualquier información
+ vinculada a su cuenta. Una vez que su cuenta es eliminada, usted puede crear
+ uno nuevo usando la misma dirección de correo electrónico.
¿Estás seguro
+ de que ¿Desea eliminar su cuenta?
+ button: Sí, continúa eliminando
+ subject: Eliminar su cuenta
+ account_reset_complete:
+ intro: Este correo electrónico confirma que ha eliminado su cuenta de login.gov.
+ subject: Cuenta borrada
+ help: ''
contact_link_text: Contáctenos
email_changed:
help: Si no desea cambiar su email, visite el %{app} %{help_link} o el %{contact_link}.
diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml
index 323d2dac763..632f237141b 100644
--- a/config/locales/user_mailer/fr.yml
+++ b/config/locales/user_mailer/fr.yml
@@ -5,6 +5,27 @@ fr:
intro: NOT TRANSLATED YET
link_text: NOT TRANSLATED YET
subject: NOT TRANSLATED YET
+ account_reset_request:
+ help: ''
+ intro: Vous avez demandé à supprimer votre compte login.gov.
Votre
+ la demande sera traitée dans les 24 heures.
Si vous ne voulez
+ pas pour supprimer votre compte, %{cancel_account_reset}.
+ subject: Supprimer votre compte
+ account_reset_granted:
+ cancel_link_text: veuillez annuler
+ help: Si vous ne souhaitez pas supprimer votre compte, %{cancel_account_reset}.
+ intro: Vous recevez cet e-mail parce que vous avez demandé à supprimer et à
+ réinitialiser votre compte login.gov.
Supprimer votre compte devrait
+ être votre dernier recours si vous êtes exclu de votre compte. Vous ne serez
+ pas en mesure de récupérer toute information liée à votre compte. Une fois
+ votre compte supprimé, vous pouvez en créer un nouveau en utilisant la même
+ adresse email.
Es-tu sûr de toi voulez-vous supprimer votre compte?
+ button: Oui, continuez la suppression
+ subject: Supprimer votre compte
+ account_reset_complete:
+ intro: Cet e-mail confirme que vous avez supprimé votre compte login.gov.
+ subject: Compte supprimé
+ help: ''
contact_link_text: communiquez avec nous
email_changed:
help: Si vous préférez ne pas changer votre adresse courriel, veuillez visiter
diff --git a/config/routes.rb b/config/routes.rb
index 5e2c3f26cdc..94d589e86c9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -32,6 +32,8 @@
as: :voice_otp,
defaults: { format: :xml }
+ post '/api/usps_upload' => 'usps_upload#create'
+
get '/openid_connect/authorize' => 'openid_connect/authorization#index'
get '/openid_connect/logout' => 'openid_connect/logout#index'
@@ -51,6 +53,16 @@
post '/' => 'users/sessions#create', as: :user_session
get '/active' => 'users/sessions#active'
+ get '/account_reset/request' => 'account_reset/request#show'
+ post '/account_reset/request' => 'account_reset/request#create'
+ get '/account_reset/cancel' => 'account_reset/cancel#cancel'
+ get '/account_reset/report_fraud' => 'account_reset/report_fraud#update'
+ get '/account_reset/confirm_request' => 'account_reset/confirm_request#show'
+ get '/account_reset/delete_account' => 'account_reset/delete_account#show'
+ delete '/account_reset/delete_account' => 'account_reset/delete_account#delete'
+ 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/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/schedule.rb b/config/schedule.rb
deleted file mode 100644
index 0ae5efdcab1..00000000000
--- a/config/schedule.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# Use this file to easily define all of your cron jobs.
-#
-# It's helpful, but not entirely necessary to understand cron before proceeding.
-# http://en.wikipedia.org/wiki/Cron
-# Learn more: http://github.com/javan/whenever
-
-set :output, '/srv/idp/shared/log/cron.log'
-env :PATH, ENV['PATH']
-
-require File.expand_path(File.dirname(__FILE__) + '/environment')
-
-health_check = Whenever.seconds(Figaro.env.queue_health_check_frequency_seconds.to_i, :seconds)
-
-every health_check, roles: [:job_creator] do
- runner 'WorkerHealthChecker.enqueue_dummy_jobs'
-end
-
-if FeatureManagement.enable_usps_verification?
- mail_batch = Whenever.seconds(Figaro.env.usps_mail_batch_hours.to_i, :hours)
-
- every mail_batch, roles: [:job_creator] do
- runner 'UspsUploader.new.run'
- end
-end
diff --git a/config/service_providers.yml b/config/service_providers.yml
index 4d414089b72..d34bef38a94 100644
--- a/config/service_providers.yml
+++ b/config/service_providers.yml
@@ -584,7 +584,7 @@ production:
- 'https://portal.dot.gov/'
restrict_to_deploy_env: 'prod'
- # NGA GEOWorks Symphony
+ # NGA GEOWorks Symphony
'urn:gov:gsa:openidconnect.profiles:sp:sso:mitre:symphony':
agency_id: 5
friendly_name: 'GEOWorks/Symphony'
@@ -628,7 +628,10 @@ production:
- 'https://office.dp3.us'
- 'https://office.dp3.us/auth/login-gov/callback'
restrict_to_deploy_env: 'prod'
-
+ attribute_bundle:
+ - x509_subject
+ - x509_presented
+
# My Move.mil
'urn:gov:gsa:openidconnect.profiles:sp:sso:dod:mymovemilprod':
agency_id: 8
@@ -643,6 +646,9 @@ production:
- 'https://my.dp3.us'
- 'https://my.dp3.us/auth/login-gov/callback'
restrict_to_deploy_env: 'prod'
+ attribute_bundle:
+ - x509_subject
+ - x509_presented
# DOT – National Registry of Certified Medical Examiners App
'urn:gov:dot:openidconnect.profiles:sp:sso:dot:nr_auth':
@@ -700,3 +706,14 @@ production:
- 'https://fossil.energy.gov/fergas-fe/#/login_redirect'
- 'https://fossil.energy.gov/fergas-fe/#/notice/15'
restrict_to_deploy_env: 'prod'
+
+ # MY CBP
+ 'urn:gov:dhs.cbp.opa.mycbp:openidconnect:prod':
+ agency_id: 1
+ friendly_name: 'MyCBP'
+ agency: 'DHS'
+ logo: 'mycbp.png'
+ return_to_sp_url: 'gov.dhs.cbp.opa.mycbp://result'
+ redirect_uris:
+ - 'gov.dhs.cbp.opa.mycbp://result'
+ restrict_to_deploy_env: 'prod'
diff --git a/db/migrate/20180619145839_create_password_metrics.rb b/db/migrate/20180619145839_create_password_metrics.rb
new file mode 100644
index 00000000000..a8492d952fe
--- /dev/null
+++ b/db/migrate/20180619145839_create_password_metrics.rb
@@ -0,0 +1,12 @@
+class CreatePasswordMetrics < ActiveRecord::Migration[5.1]
+ def change
+ create_table :password_metrics do |t|
+ t.integer :metric, null: false
+ t.float :value, null: false
+ t.integer :count, null: false
+ t.index :metric
+ t.index :value
+ t.index [:metric, :value], unique: true
+ end
+ end
+end
diff --git a/db/migrate/20180620233914_create_account_reset_requests.rb b/db/migrate/20180620233914_create_account_reset_requests.rb
new file mode 100644
index 00000000000..1d70fde3b56
--- /dev/null
+++ b/db/migrate/20180620233914_create_account_reset_requests.rb
@@ -0,0 +1,18 @@
+class CreateAccountResetRequests < ActiveRecord::Migration[5.1]
+ def change
+ create_table :account_reset_requests do |t|
+ t.integer :user_id, null: false
+ t.datetime :requested_at
+ t.string :request_token
+ t.datetime :cancelled_at
+ t.datetime :reported_fraud_at
+ t.datetime :granted_at
+ t.string :granted_token
+ t.timestamps
+ t.index ['user_id'], unique: true
+ t.index ['cancelled_at','granted_at','requested_at'], name: 'index_account_reset_requests_on_timestamps'
+ t.index ['request_token'], unique: true
+ t.index ['granted_token'], unique: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 62ed4fc141c..b5d46bfb506 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,11 +10,27 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180607144007) do
+ActiveRecord::Schema.define(version: 20180620233914) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ create_table "account_reset_requests", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.datetime "requested_at"
+ t.string "request_token"
+ t.datetime "cancelled_at"
+ t.datetime "reported_fraud_at"
+ t.datetime "granted_at"
+ t.string "granted_token"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["cancelled_at", "granted_at", "requested_at"], name: "index_account_reset_requests_on_timestamps"
+ t.index ["granted_token"], name: "index_account_reset_requests_on_granted_token", unique: true
+ t.index ["request_token"], name: "index_account_reset_requests_on_request_token", unique: true
+ t.index ["user_id"], name: "index_account_reset_requests_on_user_id", unique: true
+ end
+
create_table "agencies", force: :cascade do |t|
t.string "name", null: false
t.index ["name"], name: "index_agencies_on_name", unique: true
@@ -80,6 +96,15 @@
t.index ["updated_at"], name: "index_otp_requests_trackers_on_updated_at"
end
+ create_table "password_metrics", force: :cascade do |t|
+ t.integer "metric", null: false
+ t.float "value", null: false
+ t.integer "count", null: false
+ t.index ["metric", "value"], name: "index_password_metrics_on_metric_and_value", unique: true
+ t.index ["metric"], name: "index_password_metrics_on_metric"
+ t.index ["value"], name: "index_password_metrics_on_value"
+ end
+
create_table "profiles", force: :cascade do |t|
t.integer "user_id", null: false
t.boolean "active", default: false, null: false
diff --git a/lib/feature_management.rb b/lib/feature_management.rb
index 55eae529a8e..b0df4bc0544 100644
--- a/lib/feature_management.rb
+++ b/lib/feature_management.rb
@@ -101,4 +101,8 @@ def self.use_cloudhsm?
def self.disallow_all_web_crawlers?
Figaro.env.disallow_all_web_crawlers == 'true'
end
+
+ def self.account_reset_enabled?
+ Figaro.env.account_reset_enabled != 'false' # if value not set it defaults to enabled
+ end
end
diff --git a/lib/proofer_mocks/address_mock.rb b/lib/proofer_mocks/address_mock.rb
index ccb3649ca52..2a4983c84c8 100644
--- a/lib/proofer_mocks/address_mock.rb
+++ b/lib/proofer_mocks/address_mock.rb
@@ -5,7 +5,7 @@ class AddressMock < Proofer::Base
proof do |applicant, result|
plain_phone = applicant[:phone].gsub(/\D/, '').gsub(/\A1/, '')
- if plain_phone == '5555555555'
+ if plain_phone == '7035555555'
result.add_error(:phone, 'The phone number could not be verified.')
end
result.context[:message] = 'some context for the mock address proofer'
diff --git a/lib/rspec/formatters/user_flow_formatter.rb b/lib/rspec/formatters/user_flow_formatter.rb
index 3176080cdfc..0c54939a13d 100644
--- a/lib/rspec/formatters/user_flow_formatter.rb
+++ b/lib/rspec/formatters/user_flow_formatter.rb
@@ -70,7 +70,7 @@ def stop(_notification)
def flows_output_url
host = Rails.application.config.action_controller.asset_host || 'localhost:3000'
- protocol = host =~ /localhost/ ? 'http://' : 'https://'
+ protocol = host.match?(/localhost/) ? 'http://' : 'https://'
"#{protocol}#{host}/user_flows"
end
diff --git a/lib/tasks/account_reset.rake b/lib/tasks/account_reset.rake
new file mode 100644
index 00000000000..5b8e5ceb4f2
--- /dev/null
+++ b/lib/tasks/account_reset.rake
@@ -0,0 +1,6 @@
+namespace :account_reset do
+ desc 'Send Notifications'
+ task send_notifications: :environment do
+ AccountResetService.grant_tokens_and_send_notifications
+ end
+end
diff --git a/lib/tasks/remote_settings.rake b/lib/tasks/remote_settings.rake
index f43c6db5593..9ea98455b48 100644
--- a/lib/tasks/remote_settings.rake
+++ b/lib/tasks/remote_settings.rake
@@ -1,11 +1,11 @@
namespace :remote_settings do
- task :update, [:name, :url] => [:environment] do |task, args|
+ task :update, %i[name url] => [:environment] do |_task, args|
RemoteSettingsService.update_setting(args[:name], args[:url])
- Kernel.puts "Update successful"
+ Kernel.puts 'Update successful'
end
- task :view, [:name] => [:environment] do |task, args|
- Kernel.puts RemoteSetting.find_by(name: args[:name])&.contents
+ task :view, [:name] => [:environment] do |_task, args|
+ Kernel.puts RemoteSetting.find_by(name: args[:name])&.contents
end
task list: :environment do
@@ -14,13 +14,15 @@ namespace :remote_settings do
end
end
- task :delete, [:name] => [:environment] do |task, args|
+ task :delete, [:name] => [:environment] do |_task, args|
RemoteSetting.where(name: args[:name]).delete_all
- Kernel.puts "Delete successful"
+ Kernel.puts 'Delete successful'
end
end
+# rubocop:disable Metrics/LineLength
# example invocations:
# rake "remote_settings:update[agencies.yml,https://raw.githubusercontent.com/18F/identity-idp/master/config/agencies.yml]"
# rake "remote_settings:update[agencies.yml,https://login.gov/assets/idp/config/agencies.yml"
# rake "remote_settings:update[service_providers.yml,https://raw.githubusercontent.com/18F/identity-idp/master/config/service_providers.yml]"
+# rubocop:enable Metrics/LineLength
diff --git a/spec/controllers/account_reset/cancel_controller_spec.rb b/spec/controllers/account_reset/cancel_controller_spec.rb
new file mode 100644
index 00000000000..521243c1c37
--- /dev/null
+++ b/spec/controllers/account_reset/cancel_controller_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe AccountReset::CancelController do
+ 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})
+
+ 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})
+
+ post :cancel, params: {token:'FOO'}
+ end
+
+ it 'redirects to the root' do
+
+ post :cancel
+ expect(response).to redirect_to root_url
+ end
+ end
+end
diff --git a/spec/controllers/account_reset/confirm_delete_account_controller_spec.rb b/spec/controllers/account_reset/confirm_delete_account_controller_spec.rb
new file mode 100644
index 00000000000..1f3fcd84386
--- /dev/null
+++ b/spec/controllers/account_reset/confirm_delete_account_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+RSpec.describe AccountReset::ConfirmDeleteAccountController do
+ describe '#show' do
+ context 'email in session' do
+ it 'renders the page and deletes the email from the session' do
+ allow(controller).to receive(:flash).and_return(email: 'test@example.com')
+
+ get :show
+
+ expect(response).to render_template(:show)
+ end
+ end
+
+ context 'no email in session' do
+ it 'redirects to the new user registration path' do
+ get :show
+
+ expect(response).to redirect_to(root_url)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/account_reset/confirm_request_controller_spec.rb b/spec/controllers/account_reset/confirm_request_controller_spec.rb
new file mode 100644
index 00000000000..fc582f00a8e
--- /dev/null
+++ b/spec/controllers/account_reset/confirm_request_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+RSpec.describe AccountReset::ConfirmRequestController do
+ describe '#show' do
+ context 'email in session' do
+ it 'renders the page and deletes the email from the session' do
+ allow(controller).to receive(:flash).and_return(email: 'test@example.com')
+
+ get :show
+
+ expect(response).to render_template(:show)
+ end
+ end
+
+ context 'no email in session' do
+ it 'redirects to the new user registration path' do
+ get :show
+
+ expect(response).to redirect_to(root_url)
+ end
+ 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
new file mode 100644
index 00000000000..e045fcccc91
--- /dev/null
+++ b/spec/controllers/account_reset/delete_account_controller_spec.rb
@@ -0,0 +1,61 @@
+require 'rails_helper'
+
+describe AccountReset::DeleteAccountController do
+ describe '#delete' do
+ it 'logs a good token to the analytics' do
+ user = create(:user)
+ AccountResetService.new(user).create_request
+ AccountResetService.new(user).grant_request
+
+ 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})
+
+ delete :delete
+ end
+
+ 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})
+
+ delete :delete, params: {token:'FOO'}
+ end
+
+ it 'redirects to root if there is no token' do
+ delete :delete
+
+ expect(response).to redirect_to(root_url)
+ end
+ end
+
+ describe '#show' do
+ it 'prevents parameter leak' do
+ user = create(:user)
+ AccountResetService.new(user).create_request
+ AccountResetService.new(user).grant_request
+
+ 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'}
+
+ expect(response).to redirect_to(root_url)
+ end
+
+ it 'renders the page' do
+ user = create(:user)
+ AccountResetService.new(user).create_request
+ AccountResetService.new(user).grant_request
+ session[:granted_token] = AccountResetRequest.all[0].granted_token
+
+ get :show
+
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/account_reset/report_fraud_controller_spec.rb b/spec/controllers/account_reset/report_fraud_controller_spec.rb
new file mode 100644
index 00000000000..8268c91bb3f
--- /dev/null
+++ b/spec/controllers/account_reset/report_fraud_controller_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe AccountReset::ReportFraudController do
+ describe '#update' 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: :fraud, token_valid: true})
+
+ 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})
+
+ post :update, params: {token:'FOO'}
+ end
+
+ it 'redirects to the root' do
+
+ post :update
+ expect(response).to redirect_to root_url
+ end
+ end
+end
diff --git a/spec/controllers/account_reset/request_controller_spec.rb b/spec/controllers/account_reset/request_controller_spec.rb
new file mode 100644
index 00000000000..d002d1675ff
--- /dev/null
+++ b/spec/controllers/account_reset/request_controller_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+describe AccountReset::RequestController do
+ describe '#show' do
+ it 'renders the page' do
+ sign_in_before_2fa
+
+ get :show
+
+ expect(response).to render_template(:show)
+ end
+
+ it 'redirects to root without 2fa' do
+ get :show
+
+ expect(response).to redirect_to root_url
+ end
+
+ it 'redirects to phone setup url if 2fa not setup' do
+ user = create(:user)
+ sign_in_before_2fa(user)
+ get :show
+
+ expect(response).to redirect_to phone_setup_url
+ end
+ end
+
+ describe '#create' do
+ it 'logs the request in the analytics' do
+ TwilioService.telephony_service = FakeSms
+ sign_in_before_2fa
+
+ stub_analytics
+ expect(@analytics).to receive(:track_event).
+ with(Analytics::ACCOUNT_RESET, {event: :request})
+
+ post :create
+ end
+
+ it 'redirects to root without 2fa' do
+ post :create
+
+ expect(response).to redirect_to root_url
+ end
+
+ it 'redirects to phone setup url if 2fa not setup' do
+ user = create(:user)
+ sign_in_before_2fa(user)
+ post :create
+
+ expect(response).to redirect_to phone_setup_url
+ end
+ end
+end
diff --git a/spec/controllers/account_reset/send_notifications_controller_spec.rb b/spec/controllers/account_reset/send_notifications_controller_spec.rb
new file mode 100644
index 00000000000..b32b17be624
--- /dev/null
+++ b/spec/controllers/account_reset/send_notifications_controller_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+describe AccountReset::SendNotificationsController do
+ describe '#update' do
+ context 'with a good auth token' do
+ before do
+ headers(Figaro.env.account_reset_auth_token)
+ end
+
+ it 'returns ok' do
+ headers(Figaro.env.account_reset_auth_token)
+ post :update
+
+ expect(response).to be_ok
+ end
+
+ it 'logs the number of notifications sent in the analytics' do
+ allow(AccountResetService).to receive(:grant_tokens_and_send_notifications).and_return(7)
+
+ stub_analytics
+ expect(@analytics).to receive(:track_event).
+ with(Analytics::ACCOUNT_RESET, event: :notifications, count: 7)
+
+ post :update
+ end
+ end
+
+ context 'with a bad auth token' do
+ before do
+ headers('foo')
+ end
+
+ it 'returns unauthorized' do
+ headers('foo')
+ post :update
+
+ expect(response.status).to eq 401
+ end
+ end
+ end
+
+ def headers(token)
+ request.headers['X-API-AUTH-TOKEN'] = token
+ end
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 13f5d1437e7..f809aa3602f 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -244,6 +244,16 @@ def index
end
end
+ it 'returns a 400 bad request when a url generation error is raised on the redirect' do
+ allow_any_instance_of(ApplicationController).to \
+ receive(:redirect_to).and_raise(ActionController::UrlGenerationError)
+ allow(subject).to receive(:current_user).and_return(user)
+
+ get :index, params: { timeout: true, request_id: '123' }
+
+ expect(response).to be_bad_request
+ end
+
context 'when there is no current user' do
it 'displays a flash message' do
allow(subject).to receive(:current_user).and_return(nil)
diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb
index bcff48260b4..3b446905f8b 100644
--- a/spec/controllers/idv/phone_controller_spec.rb
+++ b/spec/controllers/idv/phone_controller_spec.rb
@@ -2,11 +2,12 @@
describe Idv::PhoneController do
include Features::LocalizationHelper
+ include IdvHelper
let(:max_attempts) { Idv::Attempter.idv_max_attempts }
- let(:good_phone) { '+1 (555) 555-0000' }
- let(:normalized_phone) { '5555550000' }
- let(:bad_phone) { '+1 (555) 555-5555' }
+ let(:good_phone) { '+1 (703) 555-0000' }
+ let(:normalized_phone) { '7035550000' }
+ let(:bad_phone) { '+1 (703) 555-5555' }
describe 'before_actions' do
it 'includes authentication before_action' do
@@ -84,7 +85,7 @@
result = {
success: false,
errors: {
- phone: [invalid_phone_message],
+ phone: [t('errors.messages.must_have_us_country_code')],
},
}
diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb
index f36d41c7944..a092d064990 100644
--- a/spec/controllers/sign_up/passwords_controller_spec.rb
+++ b/spec/controllers/sign_up/passwords_controller_spec.rb
@@ -47,6 +47,17 @@
post :create, params: { password_form: { password: 'NewVal' }, confirmation_token: token }
end
+
+ it 'saves password metrics' do
+ token = 'new token'
+ params = { password_form: { password: 'saltypickles' }, confirmation_token: token }
+ create(:user, confirmation_token: token, confirmation_sent_at: Time.zone.now)
+
+ post :create, params: params
+
+ expect(PasswordMetric.where(metric: 'length', value: 12, count: 1).count).to eq(1)
+ expect(PasswordMetric.where(metric: 'guesses_log10', value: 7.1, count: 1).count).to eq(1)
+ end
end
describe '#new' do
diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
index 626233dae02..5cee782aa58 100644
--- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
@@ -262,7 +262,7 @@
context 'phone confirmation' do
before do
sign_in_as_user
- subject.user_session[:unconfirmed_phone] = '+1 (555) 555-5555'
+ subject.user_session[:unconfirmed_phone] = '+1 (703) 555-5555'
subject.user_session[:context] = 'confirmation'
@previous_phone_confirmed_at = subject.current_user.phone_confirmed_at
subject.current_user.create_direct_otp
@@ -318,11 +318,11 @@
end
it 'does not clear session data' do
- expect(subject.user_session[:unconfirmed_phone]).to eq('+1 (555) 555-5555')
+ expect(subject.user_session[:unconfirmed_phone]).to eq('+1 (703) 555-5555')
end
it 'does not update user phone or phone_confirmed_at attributes' do
- expect(subject.current_user.phone).to eq('+1 (202) 555-1212')
+ expect(subject.current_user.phone).to eq('+1 202-555-1212')
expect(subject.current_user.phone_confirmed_at).to eq(@previous_phone_confirmed_at)
end
@@ -433,8 +433,8 @@
idv_session = Idv::Session.new(
user_session: subject.user_session, current_user: user, issuer: nil
)
- idv_session.params = { 'phone' => '+1 (555) 555-5555' }
- subject.user_session[:unconfirmed_phone] = '+1 (555) 555-5555'
+ idv_session.params = { 'phone' => '+1 (703) 555-5555' }
+ subject.user_session[:unconfirmed_phone] = '+1 (703) 555-5555'
subject.user_session[:context] = 'idv'
@previous_phone_confirmed_at = subject.current_user.phone_confirmed_at
allow(subject).to receive(:idv_session).and_return(idv_session)
@@ -489,7 +489,7 @@
end
it 'does not update user phone attributes' do
- expect(subject.current_user.reload.phone).to eq '+1 (202) 555-1212'
+ expect(subject.current_user.reload.phone).to eq '+1 202-555-1212'
expect(subject.current_user.reload.phone_confirmed_at).to eq @previous_phone_confirmed_at
end
@@ -510,11 +510,11 @@
end
it 'does not clear session data' do
- expect(subject.user_session[:unconfirmed_phone]).to eq('+1 (555) 555-5555')
+ expect(subject.user_session[:unconfirmed_phone]).to eq('+1 (703) 555-5555')
end
it 'does not update user phone or phone_confirmed_at attributes' do
- expect(subject.current_user.phone).to eq('+1 (202) 555-1212')
+ expect(subject.current_user.phone).to eq('+1 202-555-1212')
expect(subject.current_user.phone_confirmed_at).to eq(@previous_phone_confirmed_at)
expect(subject.idv_session.params['phone_confirmed_at']).to be_nil
end
diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
index b77584f0a17..6d86cd32d07 100644
--- a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
@@ -3,7 +3,7 @@
describe TwoFactorAuthentication::PivCacVerificationController do
let(:user) do
create(:user, :signed_up, :with_piv_or_cac,
- phone: '+1 (555) 555-0000')
+ phone: '+1 (703) 555-0000')
end
let(:nonce) { 'once' }
@@ -15,22 +15,22 @@
allow(subject).to receive(:user_session).and_return(session_info)
allow(PivCacService).to receive(:decode_token).with('good-token').and_return(
'uuid' => user.x509_dn_uuid,
- 'dn' => x509_subject,
+ 'subject' => x509_subject,
'nonce' => nonce
)
allow(PivCacService).to receive(:decode_token).with('good-other-token').and_return(
'uuid' => user.x509_dn_uuid + 'X',
- 'dn' => x509_subject + 'X',
+ 'subject' => x509_subject + 'X',
'nonce' => nonce
)
allow(PivCacService).to receive(:decode_token).with('bad-token').and_return(
'uuid' => 'bad-uuid',
- 'dn' => 'bad-dn',
+ 'subject' => 'bad-dn',
'nonce' => nonce
)
allow(PivCacService).to receive(:decode_token).with('bad-nonce').and_return(
'uuid' => user.x509_dn_uuid,
- 'dn' => x509_subject,
+ 'subject' => x509_subject,
'nonce' => 'bad-' + nonce
)
end
diff --git a/spec/controllers/users/phones_controller_spec.rb b/spec/controllers/users/phones_controller_spec.rb
index 320fe0ac5e7..d1cbcccf8e9 100644
--- a/spec/controllers/users/phones_controller_spec.rb
+++ b/spec/controllers/users/phones_controller_spec.rb
@@ -7,7 +7,7 @@
describe '#phone' do
let(:user) { create(:user, :signed_up, phone: '+1 (202) 555-1234') }
let(:second_user) { create(:user, :signed_up, phone: '+1 (202) 555-5678') }
- let(:new_phone) { '555-555-5555' }
+ let(:new_phone) { '202-555-4321' }
context 'user changes phone' do
before do
@@ -25,7 +25,7 @@
it 'lets user know they need to confirm their new phone' do
expect(flash[:notice]).to eq t('devise.registrations.phone_update_needs_confirmation')
- expect(user.reload.phone).to_not eq '+1 (555) 555-5555'
+ expect(user.reload.phone).to_not eq '+1 202-555-4321'
expect(@analytics).to have_received(:track_event).
with(Analytics::PHONE_CHANGE_REQUESTED)
expect(response).to redirect_to(
diff --git a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
index bc5c746dba8..03d9a4afbb2 100644
--- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
+++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
@@ -32,7 +32,7 @@
describe 'when signing in' do
before(:each) { stub_sign_in_before_2fa(user) }
let(:user) do
- create(:user, :signed_up, :with_piv_or_cac, phone: '+1 (555) 555-0000')
+ create(:user, :signed_up, :with_piv_or_cac, phone: '+1 (703) 555-0000')
end
describe 'GET index' do
@@ -55,7 +55,7 @@
context 'without associated piv/cac' do
let(:user) do
- create(:user, :signed_up, phone: '+1 (555) 555-0000')
+ create(:user, :signed_up, phone: '+1 (703) 555-0000')
end
before(:each) do
@@ -69,7 +69,7 @@
let(:good_token) { 'good-token' }
let(:good_token_response) do
{
- 'dn' => 'some dn',
+ 'subject' => 'some dn',
'uuid' => 'some-random-string',
'nonce' => nonce,
}
diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb
index 4cbcbda5b0a..db8d5220f1e 100644
--- a/spec/controllers/users/reset_passwords_controller_spec.rb
+++ b/spec/controllers/users/reset_passwords_controller_spec.rb
@@ -270,6 +270,29 @@
expect(response).to redirect_to new_user_session_path
end
end
+
+ it 'increments password metrics' do
+ raw_reset_token, db_confirmation_token =
+ Devise.token_generator.generate(User, :reset_password_token)
+
+ user = create(
+ :user,
+ :unconfirmed,
+ reset_password_token: db_confirmation_token,
+ reset_password_sent_at: Time.zone.now
+ )
+
+ stub_email_notifier(user)
+
+ password = 'saltypickles'
+ params = { password: password }
+
+ get :edit, params: { reset_password_token: raw_reset_token }
+ put :update, params: { reset_password_form: params }
+
+ expect(PasswordMetric.where(metric: 'length', value: 12, count: 1).count).to eq(1)
+ expect(PasswordMetric.where(metric: 'guesses_log10', value: 7.1, count: 1).count).to eq(1)
+ end
end
describe '#create' do
diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb
index 66f716b905c..c87b128c9da 100644
--- a/spec/controllers/users/two_factor_authentication_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb
@@ -135,7 +135,8 @@ def index
expect(SmsOtpSenderJob).to have_received(:perform_later).with(
code: subject.current_user.direct_otp,
phone: subject.current_user.phone,
- otp_created_at: subject.current_user.direct_otp_sent_at.to_s
+ otp_created_at: subject.current_user.direct_otp_sent_at.to_s,
+ locale: nil
)
expect(subject.current_user.direct_otp).not_to eq(@old_otp)
expect(subject.current_user.direct_otp).not_to be_nil
@@ -203,7 +204,8 @@ def index
expect(VoiceOtpSenderJob).to have_received(:perform_later).with(
code: subject.current_user.direct_otp,
phone: subject.current_user.phone,
- otp_created_at: subject.current_user.direct_otp_sent_at.to_s
+ otp_created_at: subject.current_user.direct_otp_sent_at.to_s,
+ locale: nil
)
expect(subject.current_user.direct_otp).not_to eq(@old_otp)
expect(subject.current_user.direct_otp).not_to be_nil
@@ -251,13 +253,14 @@ def index
expect(SmsOtpSenderJob).to have_received(:perform_now).with(
code: subject.current_user.direct_otp,
phone: @unconfirmed_phone,
- otp_created_at: subject.current_user.direct_otp_sent_at.to_s
+ otp_created_at: subject.current_user.direct_otp_sent_at.to_s,
+ locale: nil
)
end
it 'flashes an sms error when twilio responds with an sms error' do
twilio_error = Twilio::REST::RestError.new(
- '', FakeTwilioErrorResponse.new(TwilioService::SMS_ERROR_CODE)
+ '', FakeTwilioErrorResponse.new(21_614)
)
allow(SmsOtpSenderJob).to receive(:perform_now).and_raise(twilio_error)
@@ -268,7 +271,7 @@ def index
it 'flashes an invalid error when twilio responds with an invalid error' do
twilio_error = Twilio::REST::RestError.new(
- '', FakeTwilioErrorResponse.new(TwilioService::INVALID_ERROR_CODE)
+ '', FakeTwilioErrorResponse.new(21_211)
)
allow(SmsOtpSenderJob).to receive(:perform_now).and_raise(twilio_error)
@@ -279,7 +282,7 @@ def index
it 'flashes an error when twilio responds with an invalid calling area error' do
twilio_error = Twilio::REST::RestError.new(
- '', FakeTwilioErrorResponse.new(TwilioService::INVALID_CALLING_AREA_ERROR_CODE)
+ '', FakeTwilioErrorResponse.new(21_215)
)
allow(VoiceOtpSenderJob).to receive(:perform_now).and_raise(twilio_error)
@@ -291,7 +294,7 @@ def index
it 'flashes an error when twilio responds with an invalid voice number' do
twilio_error = Twilio::REST::RestError.new(
- '', FakeTwilioErrorResponse.new(TwilioService::INVALID_VOICE_NUMBER_ERROR_CODE)
+ '', FakeTwilioErrorResponse.new(13_224)
)
allow(VoiceOtpSenderJob).to receive(:perform_now).and_raise(twilio_error)
@@ -312,7 +315,7 @@ def index
expect(flash[:error]).to eq(failed_to_send_otp)
end
- it 'records an analytics event when Twilio responds with an error' do
+ it 'records an analytics event when Twilio responds with a RestError' do
stub_analytics
twilio_error = Twilio::REST::RestError.new(
'error message', FakeTwilioErrorResponse.new
@@ -337,6 +340,32 @@ def index
get :send_code, params: { otp_delivery_selection_form: { otp_delivery_preference: 'sms' } }
end
+
+ it 'records an analytics event when Twilio responds with a VerifyError' do
+ stub_analytics
+ code = 60_033
+ error_message = 'error'
+ verify_error = PhoneVerification::VerifyError.new(code: code, message: error_message)
+
+ allow(SmsOtpSenderJob).to receive(:perform_now).and_raise(verify_error)
+ analytics_hash = {
+ success: true,
+ errors: {},
+ otp_delivery_preference: 'sms',
+ resend: nil,
+ context: 'confirmation',
+ country_code: '1',
+ area_code: '202',
+ }
+
+ 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)
+
+ get :send_code, params: { otp_delivery_selection_form: { otp_delivery_preference: 'sms' } }
+ end
end
context 'when selecting an invalid delivery method' do
diff --git a/spec/controllers/users/verify_profile_phone_controller_spec.rb b/spec/controllers/users/verify_profile_phone_controller_spec.rb
index a7621443198..980d2b084f6 100644
--- a/spec/controllers/users/verify_profile_phone_controller_spec.rb
+++ b/spec/controllers/users/verify_profile_phone_controller_spec.rb
@@ -30,7 +30,7 @@
end
context 'phone is confirmed and different than 2FA' do
- let(:profile_phone) { '555-555-9999' }
+ let(:profile_phone) { '703-555-9999' }
let(:phone_confirmed) { true }
it 'redirects to OTP confirmation flow' do
diff --git a/spec/controllers/usps_upload_controller_spec.rb b/spec/controllers/usps_upload_controller_spec.rb
new file mode 100644
index 00000000000..5a9d692af91
--- /dev/null
+++ b/spec/controllers/usps_upload_controller_spec.rb
@@ -0,0 +1,59 @@
+require 'rails_helper'
+
+describe UspsUploadController do
+ describe '#create' do
+ context 'with no token' do
+ it 'returns unauthorized' do
+ post :create
+
+ expect(response.status).to eq 401
+ end
+ end
+
+ context 'with an invalid token' do
+ before do
+ headers('foobar')
+ end
+
+ it 'returns unauthorized' do
+ post :create
+
+ expect(response.status).to eq 401
+ end
+ end
+
+ context 'with a valid token' do
+ before do
+ headers(Figaro.env.usps_upload_token)
+ end
+
+ context 'on a federal workday' do
+ it 'runs the uploader' do
+ usps_uploader = instance_double(UspsUploader)
+ expect(usps_uploader).to receive(:run)
+ expect(UspsUploader).to receive(:new).and_return(usps_uploader)
+
+ post :create
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context 'on a federal holiday' do
+ it 'does not run the uploader' do
+ expect(controller).to receive(:today).and_return(Date.new(2019, 1, 1))
+
+ expect(UspsUploader).not_to receive(:new)
+
+ post :create
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+ end
+
+ def headers(token)
+ request.headers['X-USPS-UPLOAD-TOKEN'] = token
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 6410f18f62e..6e640a3f68e 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -7,7 +7,7 @@
password '!1a Z@6s' * 16 # Maximum length password.
trait :with_phone do
- phone '+1 (202) 555-1212'
+ phone '+1 202-555-1212'
phone_confirmed_at Time.zone.now
end
diff --git a/spec/features/flows/sp_authentication_flows_spec.rb b/spec/features/flows/sp_authentication_flows_spec.rb
index 6bffa8adce9..406395fcdae 100644
--- a/spec/features/flows/sp_authentication_flows_spec.rb
+++ b/spec/features/flows/sp_authentication_flows_spec.rb
@@ -334,9 +334,7 @@
def complete_phone_form_with_valid_phone
phone = Faker::PhoneNumber.cell_phone
- until PhonyRails.plausible_number? phone, country_code: :us
- phone = Faker::PhoneNumber.cell_phone
- end
+ phone = Faker::PhoneNumber.cell_phone until Phonelib.valid_for_country?(phone, 'US')
fill_in 'user_phone_form_phone', with: phone
end
end
diff --git a/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb b/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb
index fb2597d6764..493eebee4f7 100644
--- a/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb
+++ b/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb
@@ -31,24 +31,21 @@
end
end
- context 'with a voice unsupported number' do
- let(:unsupported_phone) { '242-555-5000' }
+ context 'with a non-US number' do
+ let(:bahamas_phone) { '+12423270143' }
before do
start_idv_from_sp
complete_idv_steps_before_phone_step
- fill_out_phone_form_ok(unsupported_phone)
+ fill_out_phone_form_ok(bahamas_phone)
click_idv_continue
end
- it 'sends a sms even if the user chooses voice' do
+ it 'displays an error message' do
expect(VoiceOtpSenderJob).to_not receive(:perform_later)
- expect(SmsOtpSenderJob).to receive(:perform_later)
-
- choose_idv_otp_delivery_method_voice
-
- expect(page).to_not have_content(t('links.two_factor_authentication.resend_code.phone'))
- expect(current_path).to eq(login_two_factor_path(otp_delivery_preference: :sms))
+ expect(SmsOtpSenderJob).to_not receive(:perform_later)
+ expect(page).to have_content(t('errors.messages.must_have_us_country_code'))
+ expect(current_path).to eq(idv_phone_path)
end
end
diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb
index 6de613e9e56..29c4cfbd80c 100644
--- a/spec/features/idv/steps/phone_step_spec.rb
+++ b/spec/features/idv/steps/phone_step_spec.rb
@@ -2,6 +2,7 @@
feature 'idv profile step', :idv_job do
include IdvStepHelper
+ include IdvHelper
context 'with valid information' do
it 'allows the user to continue to the phone otp delivery selection step' do
@@ -28,8 +29,8 @@
context 'after submitting valid information' do
it 'is re-entrant before confirming OTP' do
- first_phone_number = '5551231234'
- second_phone_number = '5557897890'
+ first_phone_number = '7032231234'
+ second_phone_number = '7037897890'
start_idv_from_sp
complete_idv_steps_before_phone_step
@@ -37,7 +38,7 @@
click_idv_continue
choose_idv_otp_delivery_method_sms
- expect(page).to have_content(first_phone_number)
+ expect(page).to have_content('+1 703-223-1234')
click_link t('forms.two_factor.try_again')
@@ -48,7 +49,7 @@
click_idv_continue
choose_idv_otp_delivery_method_sms
- expect(page).to have_content(second_phone_number)
+ expect(page).to have_content('+1 703-789-7890')
end
it 'is not re-entrant after confirming OTP' do
diff --git a/spec/features/idv/steps/review_step_spec.rb b/spec/features/idv/steps/review_step_spec.rb
index 595c6f1232c..1363a55045d 100644
--- a/spec/features/idv/steps/review_step_spec.rb
+++ b/spec/features/idv/steps/review_step_spec.rb
@@ -13,7 +13,7 @@
expect(page).to have_content('Nowhere, VA 6604')
expect(page).to have_content('January 02, 1980')
expect(page).to have_content('666-66-1234')
- expect(page).to have_content('+1 (555) 555-0000')
+ expect(page).to have_content('+1 202-555-1212')
fill_in 'Password', with: 'this is not the right password'
click_idv_continue
@@ -80,13 +80,13 @@
fill_in 'Password', with: user_password
click_continue
- usps_confirmation_entry = UspsConfirmation.last.decrypted_entry
+ usps_confirmation_entry = UspsConfirmation.last.entry
if sp == :saml
- expect(usps_confirmation_entry.issuer).
+ expect(usps_confirmation_entry[:issuer]).
to eq('https://rp1.serviceprovider.com/auth/saml/metadata')
else
- expect(usps_confirmation_entry.issuer).
+ expect(usps_confirmation_entry[:issuer]).
to eq('urn:gov:gsa:openidconnect:sp:server')
end
end
@@ -99,9 +99,9 @@
fill_in 'Password', with: user_password
click_continue
- usps_confirmation_entry = UspsConfirmation.last.decrypted_entry
+ usps_confirmation_entry = UspsConfirmation.last.entry
- expect(usps_confirmation_entry.issuer).to eq(nil)
+ expect(usps_confirmation_entry[:issuer]).to eq(nil)
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 c0a6be1c74c..c4ca885c936 100644
--- a/spec/features/two_factor_authentication/change_factor_spec.rb
+++ b/spec/features/two_factor_authentication/change_factor_spec.rb
@@ -21,7 +21,7 @@
allow(UserMailer).to receive(:phone_changed).with(user).and_return(mailer)
@previous_phone_confirmed_at = user.reload.phone_confirmed_at
- new_phone = '+1 (703) 555-0100'
+ new_phone = '+1 703-555-0100'
visit manage_phone_path
@@ -56,7 +56,7 @@
scenario 'editing phone number with no voice otp support only allows sms delivery' do
user.update(otp_delivery_preference: 'voice')
- unsupported_phone = '242-555-5000'
+ unsupported_phone = '242-327-0143'
visit manage_phone_path
complete_2fa_confirmation
@@ -64,7 +64,9 @@
allow(VoiceOtpSenderJob).to receive(:perform_later)
allow(SmsOtpSenderJob).to receive(:perform_now)
- update_phone_number(unsupported_phone)
+ select 'Bahamas', from: 'user_phone_form_international_code'
+ fill_in 'Phone', with: unsupported_phone
+ click_button t('forms.buttons.submit.confirm_change')
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: :sms)
expect(VoiceOtpSenderJob).to_not have_received(:perform_later)
@@ -88,7 +90,8 @@
with(
code: user.reload.direct_otp,
phone: old_phone,
- otp_created_at: user.reload.direct_otp_sent_at.to_s
+ otp_created_at: user.reload.direct_otp_sent_at.to_s,
+ locale: nil
)
expect(page).to have_content UserDecorator.new(user).masked_two_factor_phone_number
@@ -112,7 +115,8 @@
with(
code: user.reload.direct_otp,
phone: old_phone,
- otp_created_at: user.reload.direct_otp_sent_at.to_s
+ otp_created_at: user.reload.direct_otp_sent_at.to_s,
+ locale: nil
)
expect(current_path).
diff --git a/spec/features/two_factor_authentication/remember_device_spec.rb b/spec/features/two_factor_authentication/remember_device_spec.rb
index 0e965c28f49..69b0c22013a 100644
--- a/spec/features/two_factor_authentication/remember_device_spec.rb
+++ b/spec/features/two_factor_authentication/remember_device_spec.rb
@@ -29,7 +29,7 @@ def remember_device_and_sign_out_user
user = sign_up_and_set_password
user.password = Features::SessionHelper::VALID_PASSWORD
select_2fa_option('sms')
- fill_in :user_phone_form_phone, with: '5551231234'
+ fill_in :user_phone_form_phone, with: '2025551212'
click_send_security_code
check :remember_device
click_submit_default
@@ -46,7 +46,7 @@ def remember_device_and_sign_out_user
user = user_with_2fa
sign_in_and_2fa_user(user)
visit manage_phone_path
- fill_in 'user_phone_form_phone', with: '5552347193'
+ fill_in 'user_phone_form_phone', with: '2022347193'
click_button t('forms.buttons.submit.confirm_change')
check :remember_device
click_submit_default
@@ -63,7 +63,7 @@ def remember_device_and_sign_out_user
click_submit_default
visit manage_phone_path
- fill_in 'user_phone_form_phone', with: '5552347193'
+ fill_in 'user_phone_form_phone', with: '2022347193'
click_button t('forms.buttons.submit.confirm_change')
expect(current_path).to eq(login_two_factor_path(otp_delivery_preference: :sms))
@@ -81,7 +81,7 @@ def remember_device_and_sign_out_user
fill_out_idv_form_ok
click_idv_continue
click_idv_continue
- fill_out_phone_form_ok('5551603829')
+ fill_out_phone_form_ok('2022603829')
click_idv_continue
choose_idv_otp_delivery_method_sms
end
diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb
index b4b2edf38f0..61cf0da6b8a 100644
--- a/spec/features/two_factor_authentication/sign_in_spec.rb
+++ b/spec/features/two_factor_authentication/sign_in_spec.rb
@@ -24,17 +24,17 @@
submit_2fa_setup_form_with_empty_string_phone
- expect(page).to have_content invalid_phone_message
+ expect(page).to have_content t('errors.messages.missing_field')
submit_2fa_setup_form_with_invalid_phone
- expect(page).to have_content invalid_phone_message
+ expect(page).to have_content t('errors.messages.missing_field')
submit_2fa_setup_form_with_valid_phone
expect(page).to_not have_content invalid_phone_message
expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
- expect(user.reload.phone).to_not eq '+1 (555) 555-1212'
+ expect(user.reload.phone).to_not eq '+1 (703) 555-1212'
expect(user.sms?).to eq true
end
@@ -53,17 +53,17 @@
end
end
- context 'with U.S. phone that does not support voice delivery method' do
- let(:unsupported_phone) { '242-555-5555' }
+ context 'with number that does not support phone delivery method' do
+ let(:unsupported_phone) { '242-327-0143' }
- scenario 'renders an error if a user submits with voice selected' do
+ scenario 'renders an error if a user submits with JS disabled' do
sign_in_before_2fa
select_2fa_option('voice')
+ select 'Bahamas', from: 'user_phone_form_international_code'
fill_in 'Phone', with: unsupported_phone
click_send_security_code
expect(current_path).to eq phone_setup_path
-
expect(page).to have_content t(
'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported',
location: 'Bahamas'
@@ -103,6 +103,39 @@
expect(phone_field.value).to eq('12345678901234567890')
end
end
+
+ context 'with SMS option, international number, and locale header' do
+ it 'passes locale to SmsOtpSenderJob' do
+ page.driver.header 'Accept-Language', 'ar'
+ PhoneVerification.adapter = FakeAdapter
+ allow(SmsOtpSenderJob).to receive(:perform_now)
+
+ user = sign_in_before_2fa
+ select_2fa_option('sms')
+ select 'Morocco', from: 'user_phone_form_international_code'
+ fill_in 'user_phone_form_phone', with: '6 61 28 93 24'
+ click_send_security_code
+
+ expect(SmsOtpSenderJob).to have_received(:perform_now).with(
+ code: user.reload.direct_otp,
+ phone: '+212 661-289324',
+ otp_created_at: user.direct_otp_sent_at.to_s,
+ locale: 'ar'
+ )
+ expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
+ end
+ end
+
+ context 'with voice option and US number' do
+ it 'sends the code via VoiceOtpSenderJob and redirects to prompt for the code' do
+ sign_in_before_2fa
+ select_2fa_option('voice')
+ fill_in 'user_phone_form_phone', with: '7035551212'
+ click_send_security_code
+
+ expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'voice')
+ end
+ end
end
def phone_field
@@ -134,7 +167,7 @@ def submit_2fa_setup_form_with_invalid_phone
end
def submit_2fa_setup_form_with_valid_phone
- fill_in 'user_phone_form_phone', with: '555-555-1212'
+ fill_in 'user_phone_form_phone', with: '703-555-1212'
click_send_security_code
end
@@ -204,7 +237,7 @@ def submit_prefilled_otp_code
end
scenario 'the user cannot change delivery method if phone is unsupported' do
- unsupported_phone = '+1 (242) 555-5000'
+ unsupported_phone = '+1 (242) 327-0143'
user = create(:user, :signed_up, phone: unsupported_phone)
sign_in_before_2fa(user)
@@ -444,8 +477,6 @@ def submit_prefilled_otp_code
user = user_with_piv_cac
sign_in_before_2fa(user)
- click_link t('devise.two_factor_authentication.piv_cac_fallback.link')
-
expect(current_path).to eq login_two_factor_piv_cac_path
expect(page).not_to have_link(t('links.two_factor_authentication.app'))
@@ -465,8 +496,6 @@ def submit_prefilled_otp_code
user = create(:user, :signed_up, :with_piv_or_cac, otp_secret_key: 'foo')
sign_in_before_2fa(user)
- click_link t('devise.two_factor_authentication.piv_cac_fallback.link')
-
expect(current_path).to eq login_two_factor_piv_cac_path
click_link t('links.two_factor_authentication.app')
@@ -477,7 +506,6 @@ def submit_prefilled_otp_code
scenario 'user can cancel PIV/CAC process' do
user = create(:user, :signed_up, :with_piv_or_cac)
sign_in_before_2fa(user)
- click_link t('devise.two_factor_authentication.piv_cac_fallback.link')
expect(current_path).to eq login_two_factor_piv_cac_path
click_link t('links.cancel')
@@ -515,6 +543,25 @@ def submit_prefilled_otp_code
expect(current_path).to eq login_two_factor_piv_cac_path
expect(page).to have_content(t('devise.two_factor_authentication.invalid_piv_cac'))
end
+
+ context 'with SMS, international number, and locale header' do
+ it 'passes locale to SmsOtpSenderJob' do
+ page.driver.header 'Accept-Language', 'ar'
+ PhoneVerification.adapter = FakeAdapter
+ allow(SmsOtpSenderJob).to receive(:perform_later)
+
+ user = create(:user, :signed_up, phone: '+212 661-289324')
+ sign_in_user(user)
+
+ expect(SmsOtpSenderJob).to have_received(:perform_later).with(
+ code: user.reload.direct_otp,
+ phone: '+212 661-289324',
+ otp_created_at: user.direct_otp_sent_at.to_s,
+ locale: 'ar'
+ )
+ expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
+ end
+ end
end
describe 'when the user is not piv/cac enabled' do
diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb
index 4d2ae4ccb76..575ded876b6 100644
--- a/spec/features/users/sign_up_spec.rb
+++ b/spec/features/users/sign_up_spec.rb
@@ -50,7 +50,7 @@
scenario 'renders an error when twilio api responds with an error' do
twilio_error = Twilio::REST::RestError.new(
- '', FakeTwilioErrorResponse.new(TwilioService::SMS_ERROR_CODE)
+ '', FakeTwilioErrorResponse.new(21_614)
)
allow(SmsOtpSenderJob).to receive(:perform_now).and_raise(twilio_error)
diff --git a/spec/features/users/user_edit_spec.rb b/spec/features/users/user_edit_spec.rb
index 86c012d5814..f2a783447d1 100644
--- a/spec/features/users/user_edit_spec.rb
+++ b/spec/features/users/user_edit_spec.rb
@@ -53,7 +53,7 @@
allow(SmsOtpSenderJob).to receive(:perform_later)
allow(VoiceOtpSenderJob).to receive(:perform_now)
- fill_in 'user_phone_form_phone', with: '555-555-5000'
+ fill_in 'user_phone_form_phone', with: '703-555-5000'
choose 'Phone call'
click_button t('forms.buttons.submit.confirm_change')
diff --git a/spec/features/users/verify_profile_spec.rb b/spec/features/users/verify_profile_spec.rb
index e5c5d330b29..f02f6149ca2 100644
--- a/spec/features/users/verify_profile_spec.rb
+++ b/spec/features/users/verify_profile_spec.rb
@@ -8,7 +8,7 @@
profile = create(
:profile,
deactivation_reason: :verification_pending,
- pii: { ssn: '666-66-1234', dob: '1920-01-01', phone: '555-555-9999' },
+ pii: { ssn: '666-66-1234', dob: '1920-01-01', phone: '703-555-9999' },
phone_confirmed: phone_confirmed,
user: user
)
diff --git a/spec/features/visitors/phone_confirmation_spec.rb b/spec/features/visitors/phone_confirmation_spec.rb
index c9e67734309..a4b6fea9395 100644
--- a/spec/features/visitors/phone_confirmation_spec.rb
+++ b/spec/features/visitors/phone_confirmation_spec.rb
@@ -7,13 +7,14 @@
allow(SmsOtpSenderJob).to receive(:perform_now)
@user = sign_in_before_2fa
select_2fa_option('sms')
- fill_in 'user_phone_form_phone', with: '555-555-5555'
+ fill_in 'user_phone_form_phone', with: '703-555-5555'
click_send_security_code
expect(SmsOtpSenderJob).to have_received(:perform_now).with(
code: @user.reload.direct_otp,
- phone: '+1 (555) 555-5555',
- otp_created_at: @user.direct_otp_sent_at.to_s
+ phone: '+1 703-555-5555',
+ otp_created_at: @user.direct_otp_sent_at.to_s,
+ locale: nil
)
end
@@ -49,7 +50,7 @@
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 (555) 555-5555',
+ number: '+1 (703) 555-5555',
resend_code_link: t('links.two_factor_authentication.resend_code.sms'))
)
end
diff --git a/spec/forms/idv/phone_form_spec.rb b/spec/forms/idv/phone_form_spec.rb
index d4ac7eeabc1..257b0df8bbd 100644
--- a/spec/forms/idv/phone_form_spec.rb
+++ b/spec/forms/idv/phone_form_spec.rb
@@ -2,7 +2,7 @@
describe Idv::PhoneForm do
let(:user) { build_stubbed(:user, :signed_up) }
- let(:params) { { phone: '555-555-5000' } }
+ let(:params) { { phone: '703-555-5000' } }
subject { Idv::PhoneForm.new({}, user) }
@@ -52,10 +52,10 @@
end
it 'uses the user phone number as the initial phone value' do
- user = build_stubbed(:user, :signed_up, phone: '555-555-1234')
+ user = build_stubbed(:user, :signed_up, phone: '7035551234')
subject = Idv::PhoneForm.new({}, user)
- expect(subject.phone).to eq('+1 (555) 555-1234')
+ expect(subject.phone).to eq('+1 703-555-1234')
end
it 'does not use an international number as the initial phone value' do
@@ -65,11 +65,14 @@
expect(subject.phone).to eq(nil)
end
- it 'does not allow numbers with a non-US country code' do
- result = subject.submit(phone: '+81 54 354 3643')
+ it 'does not allow non-US numbers' do
+ invalid_phones = ['+81 54 354 3643', '+12423270143']
+ invalid_phones.each do |phone|
+ result = subject.submit(phone: phone)
- expect(result.success?).to eq(false)
- expect(result.errors[:phone]).to include(t('errors.messages.must_have_us_country_code'))
+ expect(result.success?).to eq(false)
+ expect(result.errors[:phone]).to include(t('errors.messages.must_have_us_country_code'))
+ end
end
end
end
diff --git a/spec/forms/update_user_password_form_spec.rb b/spec/forms/update_user_password_form_spec.rb
index 5ab385548f6..d49045b5632 100644
--- a/spec/forms/update_user_password_form_spec.rb
+++ b/spec/forms/update_user_password_form_spec.rb
@@ -65,6 +65,16 @@
expect(email_notifier).to have_received(:send_password_changed_email)
end
+
+ it 'increments password metrics for the password' do
+ params[:password] = 'saltypickles'
+ stub_email_delivery
+
+ subject.submit(params)
+
+ expect(PasswordMetric.where(metric: 'length', value: 12, count: 1).count).to eq(1)
+ expect(PasswordMetric.where(metric: 'guesses_log10', value: 7.1, count: 1).count).to eq(1)
+ end
end
context 'when the user has an active profile' do
diff --git a/spec/forms/user_phone_form_spec.rb b/spec/forms/user_phone_form_spec.rb
index 67a72c864e2..3f33cd69e19 100644
--- a/spec/forms/user_phone_form_spec.rb
+++ b/spec/forms/user_phone_form_spec.rb
@@ -1,10 +1,12 @@
require 'rails_helper'
describe UserPhoneForm do
+ include Shoulda::Matchers::ActiveModel
+
let(:user) { build(:user, :signed_up) }
let(:params) do
{
- phone: '555-555-5000',
+ phone: '703-555-5000',
international_code: 'US',
otp_delivery_preference: 'sms',
}
@@ -16,7 +18,7 @@
it 'loads initial values from the user object' do
user = build_stubbed(
:user,
- phone: '+1 (555) 500-5000',
+ phone: '+1 (703) 500-5000',
otp_delivery_preference: 'voice'
)
subject = UserPhoneForm.new(user)
@@ -33,6 +35,23 @@
expect(subject.international_code).to eq('JP')
end
+ describe 'phone validation' do
+ it do
+ should validate_inclusion_of(:international_code).
+ in_array(PhoneNumberCapabilities::INTERNATIONAL_CODES.keys)
+ end
+
+ it 'validates that the number matches the requested international code' do
+ params[:phone] = '123 123 1234'
+ params[:international_code] = 'MA'
+ result = subject.submit(params)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(false)
+ expect(result.errors).to include(:phone)
+ end
+ end
+
describe '#submit' do
context 'when phone is valid' do
it 'is valid' do
@@ -74,7 +93,7 @@
end
context 'when otp_delivery_preference is voice and phone number does not support voice' do
- let(:unsupported_phone) { '242-555-5000' }
+ let(:unsupported_phone) { '242-327-0143' }
let(:params) do
{
phone: unsupported_phone,
@@ -154,7 +173,7 @@
expect(user_updater).to receive(:call)
params = {
- phone: '555-555-5000',
+ phone: '703-555-5000',
international_code: 'US',
otp_delivery_preference: 'voice',
}
diff --git a/spec/jobs/sms_account_reset_notifier_job_spec.rb b/spec/jobs/sms_account_reset_notifier_job_spec.rb
new file mode 100644
index 00000000000..1c220653cd5
--- /dev/null
+++ b/spec/jobs/sms_account_reset_notifier_job_spec.rb
@@ -0,0 +1,40 @@
+require 'rails_helper'
+
+describe SmsAccountResetNotifierJob do
+ include Features::ActiveJobHelper
+ include Rails.application.routes.url_helpers
+
+ describe '.perform' do
+ before do
+ reset_job_queues
+ TwilioService.telephony_service = FakeSms
+ FakeSms.messages = []
+ end
+
+ subject(:perform) do
+ SmsAccountResetNotifierJob.perform_now(
+ phone: '+1 (888) 555-5555',
+ cancel_token: 'UUID1'
+ )
+ end
+
+ 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
+
+ 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_notifier_job.message', app: APP_NAME,
+ cancel_link: account_reset_cancel_url(token: 'UUID1')))
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/jobs/sms_otp_sender_job_spec.rb b/spec/jobs/sms_otp_sender_job_spec.rb
index 28b3cd46943..288fa3bd67a 100644
--- a/spec/jobs/sms_otp_sender_job_spec.rb
+++ b/spec/jobs/sms_otp_sender_job_spec.rb
@@ -82,5 +82,27 @@
expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq []
end
end
+
+ context 'when the parsed country of the phone number is not US' do
+ it 'sends the SMS via PhoneVerification class' do
+ PhoneVerification.adapter = FakeAdapter
+ phone = '+1 787-327-0143'
+ code = '123456'
+ verification = instance_double(PhoneVerification)
+ locale = 'fr'
+
+ expect(PhoneVerification).to receive(:new).
+ with(phone: phone, locale: locale, code: code).
+ and_return(verification)
+ expect(verification).to receive(:send_sms)
+
+ SmsOtpSenderJob.perform_now(
+ code: code,
+ phone: phone,
+ otp_created_at: otp_created_at,
+ locale: locale
+ )
+ end
+ end
end
end
diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb
index b2cf426938b..59d208348d7 100644
--- a/spec/lib/feature_management_spec.rb
+++ b/spec/lib/feature_management_spec.rb
@@ -431,4 +431,26 @@
expect(FeatureManagement.disallow_all_web_crawlers?).to eq(false)
end
end
+
+ describe '#account_reset_enabled?' do
+ context 'when enabled' do
+ before do
+ allow(Figaro.env).to receive(:account_reset_enabled).and_return('true')
+ end
+
+ it 'enables the feature' do
+ expect(FeatureManagement.account_reset_enabled?).to eq(true)
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ allow(Figaro.env).to receive(:account_reset_enabled).and_return('false')
+ end
+
+ it 'disables the feature' do
+ expect(FeatureManagement.account_reset_enabled?).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/tasks/rotate_rake_spec.rb b/spec/lib/tasks/rotate_rake_spec.rb
index b12a0b3d2e2..ae7fb955892 100644
--- a/spec/lib/tasks/rotate_rake_spec.rb
+++ b/spec/lib/tasks/rotate_rake_spec.rb
@@ -9,7 +9,7 @@
Rake.application.rake_require('lib/tasks/rotate', [Rails.root.to_s])
Rake::Task.define_task(:environment)
- user = create(:user, phone: '555-555-5555')
+ user = create(:user, phone: '703-555-5555')
old_email = user.email
old_phone = user.phone
old_encrypted_email = user.encrypted_email
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 1d1979e6e30..13e6c1ac0c2 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -130,4 +130,65 @@ def expect_email_body_to_have_help_and_contact_links
t('user_mailer.contact_link_text'), href: MarketingSite.contact_url
)
end
+
+ describe 'account_reset_request' do
+ let(:mail) { UserMailer.account_reset_request(user) }
+
+ it_behaves_like 'a system email'
+
+ it 'sends to the current email' do
+ expect(mail.to).to eq [user.email]
+ end
+
+ it 'renders the subject' do
+ expect(mail.subject).to eq t('user_mailer.account_reset_request.subject')
+ 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'))))
+ end
+ end
+
+ describe 'account_reset_granted' do
+ let(:mail) { UserMailer.account_reset_granted(user, user.account_reset_request) }
+
+ it_behaves_like 'a system email'
+
+ it 'sends to the current email' do
+ expect(mail.to).to eq [user.email]
+ end
+
+ it 'renders the subject' do
+ expect(mail.subject).to eq t('user_mailer.account_reset_granted.subject')
+ end
+
+ it 'renders the body' do
+ expect(mail.html_part.body).to \
+ have_content(strip_tags(t('user_mailer.account_reset_granted.intro')))
+ end
+ end
+
+ describe 'account_reset_complete' do
+ let(:mail) { UserMailer.account_reset_complete(user.email) }
+
+ it_behaves_like 'a system email'
+
+ it 'sends to the current email' do
+ expect(mail.to).to eq [user.email]
+ end
+
+ it 'renders the subject' do
+ expect(mail.subject).to eq t('user_mailer.account_reset_complete.subject')
+ end
+
+ it 'renders the body' do
+ expect(mail.html_part.body).to have_content(strip_tags(t('user_mailer.account_reset_complete.intro')))
+ end
+ end
+
+ def strip_tags(str)
+ ActionController::Base.helpers.strip_tags(str)
+ end
end
diff --git a/spec/models/account_reset_request_spec.rb b/spec/models/account_reset_request_spec.rb
new file mode 100644
index 00000000000..41ede864ac7
--- /dev/null
+++ b/spec/models/account_reset_request_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+describe AccountResetRequest do
+ it { is_expected.to belong_to(:user) }
+
+ let(:subject) { AccountResetRequest.new }
+
+ describe '#granted_token_valid?' do
+ it 'returns false if the token does not exist' do
+ subject.granted_token = nil
+ subject.granted_at = Time.zone.now
+
+ expect(subject.granted_token_valid?).to eq(false)
+ end
+
+ it 'returns false if the token is expired' do
+ subject.granted_token = '123'
+ subject.granted_at = Time.zone.now - 7.days
+
+ expect(subject.granted_token_valid?).to eq(false)
+ end
+
+ it 'returns true if the token is valid' do
+ subject.granted_token = '123'
+ subject.granted_at = Time.zone.now
+
+ expect(subject.granted_token_valid?).to eq(true)
+ end
+ end
+
+ describe '.from_valid_granted_token' do
+ it 'returns nil if the token does not exist' do
+ expect(AccountResetRequest.from_valid_granted_token('123')).to eq(nil)
+ end
+
+ it 'returns nil if the token is expired' do
+ granted_at = Time.zone.now - 7.days
+ AccountResetRequest.create(id: 1, user_id: 2, granted_token: '123', granted_at: granted_at)
+
+ expect(AccountResetRequest.from_valid_granted_token('123')).to eq(nil)
+ 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)
+
+ expect(AccountResetRequest.from_valid_granted_token('123')).to eq(arr)
+ end
+ end
+end
diff --git a/spec/models/password_metric_spec.rb b/spec/models/password_metric_spec.rb
new file mode 100644
index 00000000000..50a04f565de
--- /dev/null
+++ b/spec/models/password_metric_spec.rb
@@ -0,0 +1,48 @@
+require 'rails_helper'
+
+describe PasswordMetric do
+ describe '.increment' do
+ context 'when a metric with the given metric and value does not exists' do
+ it 'creates the metric with a count of 1' do
+ PasswordMetric.increment(:length, 10)
+
+ expect(PasswordMetric.count).to eq(1)
+ expect(PasswordMetric.find_by(metric: :length, value: 10).count).to eq(1)
+ end
+ end
+
+ context 'when a metric with the same value exists' do
+ before do
+ PasswordMetric.create(
+ metric: 'length',
+ value: 9.0,
+ count: 2
+ )
+ end
+
+ it 'creates the metric with a value of 1' do
+ PasswordMetric.increment(:length, 10)
+
+ expect(PasswordMetric.count).to eq(2)
+ expect(PasswordMetric.find_by(metric: :length, value: 10).count).to eq(1)
+ end
+ end
+
+ context 'when a metric with the given category and value does exist' do
+ before do
+ PasswordMetric.create(
+ metric: 'length',
+ value: 10.0,
+ count: 1
+ )
+ end
+
+ it 'increments the value' do
+ PasswordMetric.increment(:length, 10)
+
+ expect(PasswordMetric.count).to eq(1)
+ expect(PasswordMetric.find_by(metric: :length, value: 10).count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/models/remote_setting_spec.rb b/spec/models/remote_setting_spec.rb
index d3dd53de2b9..326627bccea 100644
--- a/spec/models/remote_setting_spec.rb
+++ b/spec/models/remote_setting_spec.rb
@@ -4,19 +4,19 @@
describe 'validations' do
it 'validates that our github repo is white listed' do
location = 'https://raw.githubusercontent.com/18F/identity-idp/master/config/agencies.yml'
- valid_setting = RemoteSetting.create(name: 'agencies.yml', url: location, contents:'')
+ valid_setting = RemoteSetting.create(name: 'agencies.yml', url: location, contents: '')
expect(valid_setting).to be_valid
end
it 'validates that the login.gov static site is white listed' do
location = 'https://login.gov/agencies.yml'
- valid_setting = RemoteSetting.create(name: 'agencies.yml', url: location, contents:'')
+ valid_setting = RemoteSetting.create(name: 'agencies.yml', url: location, contents: '')
expect(valid_setting).to be_valid
end
it 'does not accept http' do
location = 'http://login.gov/agencies.yml'
- valid_setting = RemoteSetting.create(name: 'agencies.yml', url: location, contents:'')
+ valid_setting = RemoteSetting.create(name: 'agencies.yml', url: location, contents: '')
expect(valid_setting).to_not be_valid
end
end
diff --git a/spec/models/service_provider_request_spec.rb b/spec/models/service_provider_request_spec.rb
index d42762929a9..f0799e39305 100644
--- a/spec/models/service_provider_request_spec.rb
+++ b/spec/models/service_provider_request_spec.rb
@@ -17,6 +17,11 @@
expect(ServiceProviderRequest.from_uuid('123')).
to be_an_instance_of NullServiceProviderRequest
end
+
+ it 'returns an instance of NullServiceProviderRequest when the uuid contains a null byte' do
+ expect(ServiceProviderRequest.from_uuid("\0")).
+ to be_an_instance_of NullServiceProviderRequest
+ end
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 05cd592d676..8536068dcad 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -10,6 +10,7 @@
it { is_expected.to have_many(:agency_identities) }
it { is_expected.to have_many(:profiles) }
it { is_expected.to have_many(:events) }
+ it { is_expected.to have_one(:account_reset_request) }
end
it 'does not send an email when #create is called' do
@@ -417,19 +418,4 @@
expect(user.authenticatable_salt).to eq(salt)
end
end
-
- context 'when a password is updated' do
- it 'writes encrypted_password_digest and the legacy password attributes' do
- user = create(:user)
-
- expected = {
- encrypted_password: user.encrypted_password,
- encryption_key: user.encryption_key,
- password_salt: user.password_salt,
- password_cost: user.password_cost,
- }.to_json
-
- expect(user.encrypted_password_digest).to eq(expected)
- end
- end
end
diff --git a/spec/models/usps_confirmation_spec.rb b/spec/models/usps_confirmation_spec.rb
deleted file mode 100644
index 6bfca07367c..00000000000
--- a/spec/models/usps_confirmation_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'rails_helper'
-
-describe UspsConfirmation do
- describe '#decrypted_entry' do
- it 'returns plain entry' do
- usps_confirmation = subject
- usps_confirmation.entry = UspsConfirmationEntry.new_from_hash(otp: 123).encrypted
-
- plain_entry = usps_confirmation.decrypted_entry
-
- expect(plain_entry.otp).to eq 123
- end
- end
-end
diff --git a/spec/presenters/idv/otp_delivery_method_presenter_spec.rb b/spec/presenters/idv/otp_delivery_method_presenter_spec.rb
index 597e93f87aa..e547547e1a1 100644
--- a/spec/presenters/idv/otp_delivery_method_presenter_spec.rb
+++ b/spec/presenters/idv/otp_delivery_method_presenter_spec.rb
@@ -1,8 +1,8 @@
require 'rails_helper'
describe Idv::OtpDeliveryMethodPresenter do
- let(:phone) { '555-555-0000' }
- let(:formatted_phone) { '+1 (555) 555-0000' }
+ let(:phone) { '(703) 555-0000' }
+ let(:formatted_phone) { '+1 703-555-0000' }
let(:phone_number_capabilities) { PhoneNumberCapabilities.new(formatted_phone) }
subject { Idv::OtpDeliveryMethodPresenter.new(phone) }
diff --git a/spec/presenters/openid_connect_user_info_presenter_spec.rb b/spec/presenters/openid_connect_user_info_presenter_spec.rb
index 9b722fc34d4..7a011cabcb9 100644
--- a/spec/presenters/openid_connect_user_info_presenter_spec.rb
+++ b/spec/presenters/openid_connect_user_info_presenter_spec.rb
@@ -84,7 +84,7 @@
city: 'Washington',
state: 'DC',
zipcode: '12345',
- phone: '+1 (555) 555-5555',
+ phone: '+1 (703) 555-5555',
ssn: '666661234',
}
end
@@ -102,7 +102,7 @@
expect(user_info[:given_name]).to eq('John')
expect(user_info[:family_name]).to eq('Smith')
expect(user_info[:birthdate]).to eq('1970-01-01')
- expect(user_info[:phone]).to eq('+1 (555) 555-5555')
+ expect(user_info[:phone]).to eq('+1 (703) 555-5555')
expect(user_info[:phone_verified]).to eq(true)
expect(user_info[:address]).to eq(
formatted: "123 Fake St Apt 456\nWashington, DC 12345",
@@ -132,7 +132,7 @@
expect(user_info[:given_name]).to eq(nil)
expect(user_info[:family_name]).to eq(nil)
expect(user_info[:birthdate]).to eq(nil)
- expect(user_info[:phone]).to eq('+1 (555) 555-5555')
+ expect(user_info[:phone]).to eq('+1 (703) 555-5555')
expect(user_info[:phone_verified]).to eq(true)
expect(user_info[:address]).to eq(nil)
expect(user_info[:social_security_number]).to eq(nil)
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 bc1521f30d8..3fdb8a3c622 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
@@ -53,6 +53,34 @@
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)
@@ -163,4 +191,16 @@ def presenter_with_locale(locale)
view: view
)
end
+
+ def account_reset_cancel_link(account_reset_token)
+ I18n.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_delete_account_link
+ I18n.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/services/account_reset_service_spec.rb b/spec/services/account_reset_service_spec.rb
new file mode 100644
index 00000000000..a9c3edca9d8
--- /dev/null
+++ b/spec/services/account_reset_service_spec.rb
@@ -0,0 +1,169 @@
+require 'rails_helper'
+
+describe AccountResetService do
+ include Rails.application.routes.url_helpers
+
+ let(:user) { create(:user) }
+ let(:subject) { AccountResetService.new(user) }
+ let(:user2) { create(:user) }
+ let(:subject2) { AccountResetService.new(user2) }
+
+ before {
+ allow(Figaro.env).to receive(:account_reset_wait_period_days).and_return('1')
+ }
+
+ describe '#create_request' do
+ it 'creates a new account reset request on the user' do
+ subject.create_request
+ arr = user.account_reset_request
+ expect(arr.request_token).to be_present
+ expect(arr.requested_at).to be_present
+ expect(arr.cancelled_at).to be_nil
+ expect(arr.granted_at).to be_nil
+ expect(arr.granted_token).to be_nil
+ end
+
+ it 'creates a new account reset request in the db' do
+ subject.create_request
+ arr = AccountResetRequest.find_by(user_id: user.id)
+ expect(arr.request_token).to be_present
+ expect(arr.requested_at).to be_present
+ expect(arr.cancelled_at).to be_nil
+ expect(arr.granted_at).to be_nil
+ expect(arr.granted_token).to be_nil
+ end
+ end
+
+ 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)
+ 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
+ end
+
+ it 'does not raise an error for a cancel request with a blank token' do
+ AccountResetService.cancel_request('')
+ end
+
+ it 'does not raise an error for a cancel request with a nil token' do
+ AccountResetService.cancel_request('')
+ end
+
+ it 'does not raise an error for a cancel request with a bad token' do
+ AccountResetService.cancel_request('ABC')
+ end
+ end
+
+ describe '#report_fraud' do
+ it 'removes tokens from the request' do
+ subject.create_request
+ AccountResetService.report_fraud(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.reported_fraud_at).to be_present
+ end
+
+ it 'does not raise an error for a fraud request with a blank token' do
+ token_found = AccountResetService.report_fraud('')
+ expect(token_found).to be(false)
+ end
+
+ it 'does not raise an error for a cancel request with a nil token' do
+ token_found = AccountResetService.report_fraud('')
+ expect(token_found).to be(false)
+ end
+
+ it 'does not raise an error for a cancel request with a bad token' do
+ token_found = AccountResetService.report_fraud('ABC')
+ expect(token_found).to be(false)
+ end
+ end
+
+ describe '#grant_request' do
+ it 'adds a notified at timestamp and granted token to the user' do
+ rd = subject
+ rd.create_request
+ rd.grant_request
+ arr = AccountResetRequest.find_by(user_id: user.id)
+ expect(arr.granted_at).to be_present
+ expect(arr.granted_token).to be_present
+ end
+ end
+
+ describe '.grant_tokens_and_send_notifications' do
+ context 'after waiting the full wait period' do
+ it 'does not send notifications when the notifications were already sent' do
+ subject.create_request
+
+ after_waiting_the_full_wait_period do
+ notifications_sent = 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
+ AccountResetService.cancel_request(AccountResetRequest.all[0].request_token)
+
+ after_waiting_the_full_wait_period do
+ notifications_sent = AccountResetService.grant_tokens_and_send_notifications
+ expect(notifications_sent).to eq(0)
+ end
+ end
+
+ it 'sends notifications after a request is granted' do
+ subject.create_request
+
+ after_waiting_the_full_wait_period do
+ notifications_sent = AccountResetService.grant_tokens_and_send_notifications
+
+ expect(notifications_sent).to eq(1)
+ end
+ end
+
+ it 'sends 2 notifications after 2 requests are granted' do
+ subject.create_request
+ subject2.create_request
+
+ after_waiting_the_full_wait_period do
+ notifications_sent = AccountResetService.grant_tokens_and_send_notifications
+
+ expect(notifications_sent).to eq(2)
+ end
+ end
+ end
+
+ context 'after not waiting the full wait period' do
+ it 'does not send notifications after a request' do
+ subject.create_request
+
+ notifications_sent = AccountResetService.grant_tokens_and_send_notifications
+ expect(notifications_sent).to eq(0)
+ end
+
+ it 'does not send notifications when the request was cancelled' do
+ arr = subject.create_request
+ AccountResetService.cancel_request(AccountResetRequest.all[0].request_token)
+
+ notifications_sent = AccountResetService.grant_tokens_and_send_notifications
+ expect(notifications_sent).to eq(0)
+ end
+ end
+ end
+
+ def after_waiting_the_full_wait_period
+ TwilioService.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
diff --git a/spec/services/agency_seeder_spec.rb b/spec/services/agency_seeder_spec.rb
index a02cb3798bb..280592200a9 100644
--- a/spec/services/agency_seeder_spec.rb
+++ b/spec/services/agency_seeder_spec.rb
@@ -36,7 +36,9 @@
context 'when agencies.yml has a remote setting' do
before do
location = 'https://raw.githubusercontent.com/18F/identity-idp/master/config/agencies.yml'
- RemoteSetting.create(name: 'agencies.yml', url:location, contents: "test:\n 1:\n name: 'CBP'")
+ RemoteSetting.create(
+ name: 'agencies.yml', url: location, contents: "test:\n 1:\n name: 'CBP'"
+ )
end
it 'updates the attributes based on the current value of the yml file' do
diff --git a/spec/services/encrypted_attribute_spec.rb b/spec/services/encrypted_attribute_spec.rb
index bb627374916..c3e208640fa 100644
--- a/spec/services/encrypted_attribute_spec.rb
+++ b/spec/services/encrypted_attribute_spec.rb
@@ -48,12 +48,21 @@
describe '#stale?' do
it 'returns true when email was encrypted with old key' do
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('false')
encrypted_with_old_key = encrypted_email
rotate_attribute_encryption_key
expect(EncryptedAttribute.new(encrypted_with_old_key).stale?).to eq true
end
+ it 'returns true with legacy encryption and old key now switched to encryption without kms' do
+ encrypted_with_old_key = encrypted_email
+ rotate_attribute_encryption_key
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('true')
+
+ expect(EncryptedAttribute.new(encrypted_with_old_key).stale?).to eq true
+ end
+
it 'returns false when email was encrypted with current key' do
ee = EncryptedAttribute.new(encrypted_email)
diff --git a/spec/services/encryption/encryptors/attribute_encryptor_spec.rb b/spec/services/encryption/encryptors/attribute_encryptor_spec.rb
index 6906062a1bb..e7c3ee19275 100644
--- a/spec/services/encryption/encryptors/attribute_encryptor_spec.rb
+++ b/spec/services/encryption/encryptors/attribute_encryptor_spec.rb
@@ -8,8 +8,6 @@
let(:retired_cost) { '2000$8$1$' }
before do
- described_class.instance_variable_set(:@_scypt_hashes_by_key, nil)
-
allow(Figaro.env).to receive(:attribute_encryption_key).and_return(current_key)
allow(Figaro.env).to receive(:attribute_cost).and_return(current_cost)
allow(Figaro.env).to receive(:attribute_encryption_key_queue).and_return(
@@ -18,73 +16,129 @@
end
describe '#encrypt' do
- it 'returns encrypted text' do
- ciphertext = subject.encrypt(plaintext)
+ context 'with old kms based encryption' do
+ before do
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('false')
+ end
- expect(ciphertext).to_not eq(plaintext)
- end
+ it 'returns encrypted text' do
+ ciphertext = subject.encrypt(plaintext)
- it 'only computes an scrypt hash the first time a key is used' do
- expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
+ expect(ciphertext).to_not eq(plaintext)
+ end
+ end
- subject.encrypt(plaintext)
+ context 'with new non kms based encrytpion' do
+ before do
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('true')
+ end
- expect(SCrypt::Engine).to_not receive(:hash_secret)
+ it 'returns encrypted text' do
+ ciphertext = subject.encrypt(plaintext)
- subject.encrypt(plaintext)
+ expect(ciphertext).to_not eq(plaintext)
+ end
end
end
describe '#decrypt' do
- let(:ciphertext) do
- result = subject.encrypt(plaintext)
- described_class.instance_variable_set(:@_scypt_hashes_by_key, nil)
- result
- end
+ context 'with old kms based encryption' do
+ let(:ciphertext) do
+ subject.encrypt(plaintext)
+ end
- before do
- # Memoize the ciphertext and purge the key pool so that encryption does not
- # affect expected call counts
- ciphertext
- end
+ before do
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('false')
+ # Memoize the ciphertext and purge the key pool so that encryption does not
+ # affect expected call counts
+ ciphertext
+ end
- context 'with a ciphertext made with the current key' do
- it 'decrypts the ciphertext' do
- expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ context 'with a ciphertext made with the current key' do
+ it 'decrypts the ciphertext' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
end
- it 'only computes an scrypt hash the first time a keys is used' do
- expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
+ context 'after rotating keys' do
+ before do
+ rotate_attribute_encryption_key
+ end
- subject.decrypt(ciphertext)
+ it 'tries to decrypt with successive keys until it is successful' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
+ end
- expect(SCrypt::Engine).to_not receive(:hash_secret)
+ context 'it migrates legacy encrypted data after rotating keys' do
+ before do
+ rotate_attribute_encryption_key
+ end
- subject.decrypt(ciphertext)
+ it 'tries to decrypt with successive keys until it is successful' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
+ end
+
+ context 'with a ciphertext made with a key that does not exist' do
+ before do
+ rotate_attribute_encryption_key_with_invalid_queue
+ end
+
+ it 'raises and encryption error' do
+ expect { subject.decrypt(ciphertext) }.to \
+ raise_error(Encryption::EncryptionError, 'unable to decrypt attribute with any key')
+ end
end
end
- context 'after rotating keys' do
+ context 'with new new non kms based encryption' do
+ let(:ciphertext) do
+ subject.encrypt(plaintext)
+ end
+
before do
- rotate_attribute_encryption_key
+ # Memoize the ciphertext and purge the key pool so that encryption does not
+ # affect expected call counts
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('true')
+ ciphertext
end
- it 'tries to decrypt with successive keys until it is successful' do
- expect(Encryption::UserAccessKey).to receive(:new).twice.and_call_original
+ context 'with a ciphertext made with the current key' do
+ it 'decrypts the ciphertext' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
+ end
+
+ context 'after rotating keys' do
+ before do
+ rotate_attribute_encryption_key
+ end
- expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ it 'tries to decrypt with successive keys until it is successful' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
end
- end
- context 'with a ciphertext made with a key that does not exist' do
- before do
- rotate_attribute_encryption_key_with_invalid_queue
+ context 'it migrates legacy encrypted data after rotating keys' do
+ before do
+ rotate_attribute_encryption_key
+ end
+
+ it 'tries to decrypt with successive keys until it is successful' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
end
- it 'raises and encryption error' do
- expect { subject.decrypt(ciphertext) }.to raise_error(
- Encryption::EncryptionError, 'unable to decrypt attribute with any key'
- )
+ context 'with a ciphertext made with a key that does not exist' do
+ before do
+ rotate_attribute_encryption_key_with_invalid_queue
+ end
+
+ it 'raises and encryption error' do
+ expect { subject.decrypt(ciphertext) }.to \
+ raise_error(Encryption::EncryptionError, 'unable to decrypt attribute with any key')
+ end
end
end
end
@@ -98,23 +152,21 @@
end
it 'returns true if an old key was last used to decrypt something' do
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('false')
ciphertext = subject.encrypt(plaintext)
rotate_attribute_encryption_key
subject.decrypt(ciphertext)
expect(subject.stale?).to eq(true)
end
- end
-
- describe '.load_or_init_user_access_key' do
- it 'does not return the same key object for the same salt and cost' do
- expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
- key1 = described_class.load_or_init_user_access_key(key: current_key, cost: current_cost)
- key2 = described_class.load_or_init_user_access_key(key: current_key, cost: current_cost)
+ it 'returns true if old key used to decrypt and we turn on new encryption' do
+ ciphertext = subject.encrypt(plaintext)
+ rotate_attribute_encryption_key
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('true')
+ subject.decrypt(ciphertext)
- expect(key1.as_scrypt_hash).to eq(key2.as_scrypt_hash)
- expect(key1).to_not eq(key2)
+ expect(subject.stale?).to eq(true)
end
end
end
diff --git a/spec/services/encryption/encryptors/deprecated_attribute_encryptor_spec.rb b/spec/services/encryption/encryptors/deprecated_attribute_encryptor_spec.rb
new file mode 100644
index 00000000000..0b4eb8a507d
--- /dev/null
+++ b/spec/services/encryption/encryptors/deprecated_attribute_encryptor_spec.rb
@@ -0,0 +1,120 @@
+require 'rails_helper'
+
+describe Encryption::Encryptors::DeprecatedAttributeEncryptor do
+ let(:plaintext) { 'some secret text' }
+ let(:current_key) { '1' * 32 }
+ let(:current_cost) { '400$8$1$' }
+ let(:retired_key) { '2' * 32 }
+ let(:retired_cost) { '2000$8$1$' }
+
+ before do
+ described_class.instance_variable_set(:@_scypt_hashes_by_key, nil)
+
+ allow(Figaro.env).to receive(:attribute_encryption_key).and_return(current_key)
+ allow(Figaro.env).to receive(:attribute_cost).and_return(current_cost)
+ allow(Figaro.env).to receive(:attribute_encryption_key_queue).and_return(
+ [{ key: retired_key, cost: retired_cost }].to_json
+ )
+ end
+
+ describe '#encrypt' do
+ it 'returns encrypted text' do
+ ciphertext = subject.encrypt(plaintext)
+
+ expect(ciphertext).to_not eq(plaintext)
+ end
+
+ it 'only computes an scrypt hash the first time a key is used' do
+ expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
+
+ subject.encrypt(plaintext)
+
+ expect(SCrypt::Engine).to_not receive(:hash_secret)
+
+ subject.encrypt(plaintext)
+ end
+ end
+
+ describe '#decrypt' do
+ let(:ciphertext) do
+ result = subject.encrypt(plaintext)
+ described_class.instance_variable_set(:@_scypt_hashes_by_key, nil)
+ result
+ end
+
+ before do
+ # Memoize the ciphertext and purge the key pool so that encryption does not
+ # affect expected call counts
+ ciphertext
+ end
+
+ context 'with a ciphertext made with the current key' do
+ it 'decrypts the ciphertext' do
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
+
+ it 'only computes an scrypt hash the first time a keys is used' do
+ expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
+
+ subject.decrypt(ciphertext)
+
+ expect(SCrypt::Engine).to_not receive(:hash_secret)
+
+ subject.decrypt(ciphertext)
+ end
+ end
+
+ context 'after rotating keys' do
+ before do
+ rotate_attribute_encryption_key
+ end
+
+ it 'tries to decrypt with successive keys until it is successful' do
+ expect(Encryption::UserAccessKey).to receive(:new).twice.and_call_original
+
+ expect(subject.decrypt(ciphertext)).to eq(plaintext)
+ end
+ end
+
+ context 'with a ciphertext made with a key that does not exist' do
+ before do
+ rotate_attribute_encryption_key_with_invalid_queue
+ end
+
+ it 'raises and encryption error' do
+ expect { subject.decrypt(ciphertext) }.to raise_error(
+ Encryption::EncryptionError, 'unable to decrypt attribute with any key'
+ )
+ end
+ end
+ end
+
+ describe '#stale?' do
+ it 'returns false if the current key last was used to decrypt something' do
+ ciphertext = subject.encrypt(plaintext)
+ subject.decrypt(ciphertext)
+
+ expect(subject.stale?).to eq(false)
+ end
+
+ it 'returns true if an old key was last used to decrypt something' do
+ ciphertext = subject.encrypt(plaintext)
+ rotate_attribute_encryption_key
+ subject.decrypt(ciphertext)
+
+ expect(subject.stale?).to eq(true)
+ end
+ end
+
+ describe '.load_or_init_user_access_key' do
+ it 'does not return the same key object for the same salt and cost' do
+ expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
+
+ key1 = described_class.load_or_init_user_access_key(key: current_key, cost: current_cost)
+ key2 = described_class.load_or_init_user_access_key(key: current_key, cost: current_cost)
+
+ expect(key1.as_scrypt_hash).to eq(key2.as_scrypt_hash)
+ expect(key1).to_not eq(key2)
+ end
+ end
+end
diff --git a/spec/services/encryption/kms_client_spec.rb b/spec/services/encryption/kms_client_spec.rb
index 29469b966de..9d52eaa79ea 100644
--- a/spec/services/encryption/kms_client_spec.rb
+++ b/spec/services/encryption/kms_client_spec.rb
@@ -69,4 +69,14 @@
end
end
end
+
+ describe '#looks_like_kms?' do
+ it 'returns true for kms encrypted data' do
+ expect(subject.class.looks_like_kms?('KMSx' + kms_ciphertext)).to eq(true)
+ end
+
+ it 'returns false for non kms encrypted data' do
+ expect(subject.class.looks_like_kms?('abcdef.' + kms_ciphertext)).to eq(false)
+ end
+ end
end
diff --git a/spec/services/encryption/password_verifier_spec.rb b/spec/services/encryption/password_verifier_spec.rb
index 6fd9d0e6212..5f045ad2b8b 100644
--- a/spec/services/encryption/password_verifier_spec.rb
+++ b/spec/services/encryption/password_verifier_spec.rb
@@ -41,4 +41,10 @@
expect(result).to eq(false)
end
end
+
+ it 'raises an encryption error when the password digest is nil' do
+ expect do
+ Encryption::PasswordVerifier::PasswordDigest.parse_from_string(nil)
+ end.to raise_error(Encryption::EncryptionError)
+ end
end
diff --git a/spec/services/file_encryptor_spec.rb b/spec/services/file_encryptor_spec.rb
deleted file mode 100644
index aeba9fd5621..00000000000
--- a/spec/services/file_encryptor_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe FileEncryptor do
- let(:email) { Figaro.env.equifax_gpg_email }
-
- subject(:file_encryptor) do
- FileEncryptor.new(
- Rails.root.join('keys', 'equifax_gpg.pub.bin'),
- email
- )
- end
-
- let(:output_file) { Tempfile.new('temp.encrypted') }
-
- after do
- output_file.close
- output_file.unlink
- end
-
- describe '#encrypt' do
- let(:plaintext) { 'aaa' }
-
- subject(:encrypt) { file_encryptor.encrypt(plaintext, output_file.path) }
-
- it 'writes the encrypted content to a file' do
- encrypt
-
- encrypted_content = File.binread(output_file.path)
-
- expect(encrypted_content).to be_present
- expect(encrypted_content).to_not include(plaintext)
- end
-
- context 'with a bad email' do
- let(:email) { 'aaa@aaa.com' }
-
- it 'raises an error' do
- expect { encrypt }.to raise_error(FileEncryptor::EncryptionError)
- end
- end
- end
-
- describe '#decrypt' do
- let(:passphrase) { Figaro.env.equifax_development_example_gpg_passphrase }
- let(:plaintext) { 'some super secret content' }
-
- before do
- file_encryptor.encrypt(plaintext, output_file.path)
- end
-
- it 'returns the decrypted content' do
- decrypted = file_encryptor.decrypt(passphrase, output_file.path)
-
- expect(decrypted).to eq(plaintext)
- end
- end
-end
diff --git a/spec/services/holiday_service_spec.rb b/spec/services/holiday_service_spec.rb
new file mode 100644
index 00000000000..4bd7b6e21c1
--- /dev/null
+++ b/spec/services/holiday_service_spec.rb
@@ -0,0 +1,155 @@
+require 'rails_helper'
+
+RSpec.describe HolidayService do
+ let(:year) { 2018 }
+
+ let(:instance) { described_class.new(year) }
+
+ context 'instance methods' do
+ describe '#holidays' do
+ subject { instance.holidays }
+
+ it { is_expected.to eq(holidays) }
+ end
+
+ describe '#observed_holidays' do
+ subject { instance.observed_holidays }
+
+ it { is_expected.to eq(observed_holidays) }
+
+ context 'when the next NY is on a Saturday' do
+ let(:year) { 2010 }
+
+ subject { instance.observed_holidays }
+
+ it 'includes Dec 31st' do
+ expect(subject).to include(Date.new(2010, 12, 31))
+ end
+ end
+ end
+
+ describe '#holiday?' do
+ subject { instance.holiday?(date) }
+
+ context 'when its a holiday' do
+ let(:date) { Date.new(year, 11, 11) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when its not a holiday' do
+ let(:date) { Date.new(year, 11, 12) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#observed_holiday?' do
+ subject { instance.observed_holiday?(date) }
+
+ context 'when its not a holiday' do
+ let(:date) { Date.new(year, 11, 11) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when its a holiday' do
+ let(:date) { Date.new(year, 11, 12) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe '#observed' do
+ subject { instance.send(:observed, date) }
+
+ context 'when date is a Saturday' do
+ let(:date) { Date.new(2018, 6, 23) }
+
+ it 'returns the day before' do
+ expect(subject).to eq(date - 1)
+ end
+ end
+
+ context 'when date is a Sunday' do
+ let(:date) { Date.new(2018, 6, 24) }
+
+ it 'returns the day after' do
+ expect(subject).to eq(date + 1)
+ end
+ end
+
+ context 'when date is a weekday' do
+ let(:date) { Date.new(2018, 6, 25) }
+
+ it 'returns the day' do
+ expect(subject).to eq(date)
+ end
+ end
+ end
+ end
+
+ context 'class methods' do
+ describe '.holiday?' do
+ subject { described_class.holiday?(date) }
+
+ context 'when its a holiday' do
+ let(:date) { Date.new(year, 11, 11) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when its not a holiday' do
+ let(:date) { Date.new(year, 11, 12) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.observed_holiday?' do
+ subject { described_class.observed_holiday?(date) }
+
+ context 'when its not a holiday' do
+ let(:date) { Date.new(year, 11, 11) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when its a holiday' do
+ let(:date) { Date.new(year, 11, 12) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
+
+ def holidays
+ [
+ Date.new(year, 1, 1),
+ Date.new(year, 1, 15),
+ Date.new(year, 2, 19),
+ Date.new(year, 5, 28),
+ Date.new(year, 7, 4),
+ Date.new(year, 9, 3),
+ Date.new(year, 10, 8),
+ Date.new(year, 11, 11),
+ Date.new(year, 11, 22),
+ Date.new(year, 12, 25),
+ ]
+ end
+
+ def observed_holidays
+ [
+ Date.new(year, 1, 1),
+ Date.new(year, 1, 15),
+ Date.new(year, 2, 19),
+ Date.new(year, 5, 28),
+ Date.new(year, 7, 4),
+ Date.new(year, 9, 3),
+ Date.new(year, 10, 8),
+ Date.new(year, 11, 12),
+ Date.new(year, 11, 22),
+ Date.new(year, 12, 25),
+ ]
+ end
+end
diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb
index 74637a5d93b..093ccc89418 100644
--- a/spec/services/idv/phone_step_spec.rb
+++ b/spec/services/idv/phone_step_spec.rb
@@ -9,7 +9,7 @@
idvs.applicant = { first_name: 'Some' }
idvs
end
- let(:idv_form_params) { { phone: '555-555-0000', phone_confirmed_at: nil } }
+ let(:idv_form_params) { { phone: '703-555-0000', phone_confirmed_at: nil } }
let(:idv_phone_form) { Idv::PhoneForm.new(idv_session.params, user) }
def build_step(vendor_validator_result)
@@ -49,7 +49,7 @@ def build_step(vendor_validator_result)
end
it 'returns false for mock-sad phone' do
- idv_form_params[:phone] = '555-555-5555'
+ idv_form_params[:phone] = '703-555-5555'
errors = { phone: ['The phone number could not be verified.'] }
step = build_step(
diff --git a/spec/services/openid_connect_attribute_scoper_spec.rb b/spec/services/openid_connect_attribute_scoper_spec.rb
index e0e9888abb9..5eb7522a644 100644
--- a/spec/services/openid_connect_attribute_scoper_spec.rb
+++ b/spec/services/openid_connect_attribute_scoper_spec.rb
@@ -36,7 +36,7 @@
given_name: 'John',
family_name: 'Jones',
birthdate: '1970-01-01',
- phone: '+1 (555) 555-5555',
+ phone: '+1 (703) 555-5555',
phone_verified: true,
address: {
formatted: "123 Fake St\nWashington, DC 12345",
diff --git a/spec/services/password_metrics_incrementer_spec.rb b/spec/services/password_metrics_incrementer_spec.rb
new file mode 100644
index 00000000000..bfbd18758c1
--- /dev/null
+++ b/spec/services/password_metrics_incrementer_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+describe PasswordMetricsIncrementer do
+ let(:password) { 'saltypickles' }
+ let(:guesses_log10) { 7.1 }
+
+ subject { described_class.new(password) }
+
+ describe '#increment_password_metrics' do
+ it 'increments password metrics for the length and guesses' do
+ subject.increment_password_metrics
+
+ expect(PasswordMetric.where(metric: 'length', value: 12, count: 1).count).to eq(1)
+ expect(PasswordMetric.where(metric: 'guesses_log10', value: 7.1, count: 1).count).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/phone_formatter_spec.rb b/spec/services/phone_formatter_spec.rb
index 8f32b271a2d..56aa36d77ad 100644
--- a/spec/services/phone_formatter_spec.rb
+++ b/spec/services/phone_formatter_spec.rb
@@ -3,42 +3,42 @@
describe PhoneFormatter do
describe '#format' do
it 'formats international numbers correctly' do
- phone = '+404004004000'
- formatted_phone = PhoneFormatter.new.format(phone)
+ phone = '+40211234567'
+ formatted_phone = PhoneFormatter.format(phone)
- expect(formatted_phone).to eq('+40 400 400 4000')
+ expect(formatted_phone).to eq('+40 21 123 4567')
end
it 'formats U.S. numbers correctly' do
phone = '+12025005000'
- formatted_phone = PhoneFormatter.new.format(phone)
+ formatted_phone = PhoneFormatter.format(phone)
- expect(formatted_phone).to eq('+1 (202) 500-5000')
+ expect(formatted_phone).to eq('+1 202-500-5000')
end
it 'uses +1 as the default international code' do
phone = '2025005000'
- formatted_phone = PhoneFormatter.new.format(phone)
+ formatted_phone = PhoneFormatter.format(phone)
- expect(formatted_phone).to eq('+1 (202) 500-5000')
+ expect(formatted_phone).to eq('+1 202-500-5000')
end
it 'uses the international code for the country specified in the country code option' do
- phone = '123123123'
- formatted_phone = PhoneFormatter.new.format(phone, country_code: 'MA')
+ phone = '636023853'
+ formatted_phone = PhoneFormatter.format(phone, country_code: 'MA')
- expect(formatted_phone).to eq('+212 12 3123 123')
+ expect(formatted_phone).to eq('+212 636-023853')
end
it 'returns nil for nil' do
- formatted_phone = PhoneFormatter.new.format(nil)
+ formatted_phone = PhoneFormatter.format(nil)
expect(formatted_phone).to be_nil
end
it 'returns nil for nonsense' do
phone = '☎️📞📱📳'
- formatted_phone = PhoneFormatter.new.format(phone)
+ formatted_phone = PhoneFormatter.format(phone)
expect(formatted_phone).to be_nil
end
end
diff --git a/spec/services/phone_number_capabilities_spec.rb b/spec/services/phone_number_capabilities_spec.rb
index fb50c947300..4b9afdc5887 100644
--- a/spec/services/phone_number_capabilities_spec.rb
+++ b/spec/services/phone_number_capabilities_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
describe PhoneNumberCapabilities do
- let(:phone) { '+1 (555) 555-5000' }
+ let(:phone) { '+1 (703) 555-5000' }
subject { PhoneNumberCapabilities.new(phone) }
describe '#sms_only?' do
@@ -10,7 +10,7 @@
end
context 'voice is not supported for the area code' do
- let(:phone) { '+1 (242) 555-5000' }
+ let(:phone) { '+1 (242) 327-0143' }
it { expect(subject.sms_only?).to eq(true) }
end
@@ -28,7 +28,7 @@
describe '#unsupported_location' do
it 'returns the name of the unsupported area code location' do
- locality = PhoneNumberCapabilities.new('+1 (242) 555-5000').unsupported_location
+ locality = PhoneNumberCapabilities.new('+1 (242) 327-0143').unsupported_location
expect(locality).to eq('Bahamas')
end
diff --git a/spec/services/phone_verification_spec.rb b/spec/services/phone_verification_spec.rb
new file mode 100644
index 00000000000..dbbc692fd1a
--- /dev/null
+++ b/spec/services/phone_verification_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+describe PhoneVerification do
+ describe '#send_sms' do
+ it 'makes a POST request to Twilio Verify endpoint' do
+ PhoneVerification.adapter = FakeAdapter
+
+ phone = '17873270143'
+ headers = { 'X-Authy-API-Key' => 'secret' }
+ locale = 'es'
+ code = '123456'
+ body = {
+ code_length: 6,
+ country_code: '1',
+ custom_code: code,
+ locale: locale,
+ phone_number: '7873270143',
+ via: 'sms',
+ }
+ connecttimeout = PhoneVerification::OPEN_TIMEOUT
+ timeout = PhoneVerification::READ_TIMEOUT
+
+ expect(FakeAdapter).to receive(:post).
+ with(
+ PhoneVerification::AUTHY_START_ENDPOINT,
+ headers: headers,
+ body: body,
+ connecttimeout: connecttimeout,
+ timeout: timeout
+ ).and_return(FakeAdapter::SuccessResponse.new)
+
+ PhoneVerification.new(phone: phone, locale: locale, code: code).send_sms
+ end
+
+ it 'raises VerifyError when response is not successful' do
+ PhoneVerification.adapter = FakeAdapter
+ phone = '17035551212'
+ code = '123456'
+
+ allow(FakeAdapter).to receive(:post).and_return(FakeAdapter::ErrorResponse.new)
+
+ expect { PhoneVerification.new(phone: phone, code: code).send_sms }.to raise_error do |error|
+ expect(error.code).to eq 60_033
+ expect(error.message).to eq 'Invalid number'
+ expect(error).to be_a(PhoneVerification::VerifyError)
+ end
+ end
+ end
+end
diff --git a/spec/services/pii/cacher_spec.rb b/spec/services/pii/cacher_spec.rb
index 36b0e0d3ca3..bf5c62c1507 100644
--- a/spec/services/pii/cacher_spec.rb
+++ b/spec/services/pii/cacher_spec.rb
@@ -11,6 +11,7 @@
describe '#save' do
before do
+ allow(Figaro.env).to receive(:attribute_encryption_without_kms).and_return('false')
allow(FeatureManagement).to receive(:use_kms?).and_return(false)
profile.save!
end
diff --git a/spec/services/piv_cac_service_spec.rb b/spec/services/piv_cac_service_spec.rb
index 78446c738f5..b9127b7599b 100644
--- a/spec/services/piv_cac_service_spec.rb
+++ b/spec/services/piv_cac_service_spec.rb
@@ -37,10 +37,10 @@
end
it 'returns the test data' do
- token = 'TEST:{"uuid":"hijackedUUID","dn":"hijackedDN"}'
+ token = 'TEST:{"uuid":"hijackedUUID","subject":"hijackedDN"}'
expect(PivCacService.decode_token(token)).to eq(
'uuid' => 'hijackedUUID',
- 'dn' => 'hijackedDN'
+ 'subject' => 'hijackedDN'
)
end
end
@@ -106,11 +106,11 @@
stub_request(:post, 'localhost:8443').
with(
body: 'token=foo',
- headers: {'Authentication' => %r<^hmac\s+:.+:.+$>}
+ headers: { 'Authentication' => /^hmac\s+:.+:.+$/ }
).
to_return(
status: [200, 'Ok'],
- body: '{"dn":"dn","uuid":"uuid"}'
+ body: '{"subject":"dn","uuid":"uuid"}'
)
end
@@ -121,14 +121,14 @@
it 'returns the decoded JSON from the target service' do
expect(PivCacService.decode_token('foo')).to eq(
- 'dn' => 'dn',
+ 'subject' => 'dn',
'uuid' => 'uuid'
)
end
describe 'with test data' do
it 'returns an error' do
- token = 'TEST:{"uuid":"hijackedUUID","dn":"hijackedDN"}'
+ token = 'TEST:{"uuid":"hijackedUUID","subject":"hijackedDN"}'
expect(PivCacService.decode_token(token)).to eq(
'error' => 'token.bad'
)
diff --git a/spec/services/remote_settings_service_spec.rb b/spec/services/remote_settings_service_spec.rb
index 5b499f88dc6..1bb24b4e293 100644
--- a/spec/services/remote_settings_service_spec.rb
+++ b/spec/services/remote_settings_service_spec.rb
@@ -2,9 +2,7 @@
describe RemoteSettingsService do
subject(:service) { RemoteSettingsService }
- before {
- WebMock.allow_net_connect!
- }
+ before { WebMock.allow_net_connect! }
describe '.load_yml_erb' do
it 'loads the remote location' do
@@ -34,13 +32,11 @@
end
end
-
describe '.load' do
it 'loads the remote location' do
expect do
service.load('https://github.com/18F/identity-idp/blob/master/public/images/logo.svg')
end.to_not raise_error
-
end
it 'raises an error if the location is not https://' do
diff --git a/spec/services/service_provider_seeder_spec.rb b/spec/services/service_provider_seeder_spec.rb
index a2990dd16b3..215b92be669 100644
--- a/spec/services/service_provider_seeder_spec.rb
+++ b/spec/services/service_provider_seeder_spec.rb
@@ -116,7 +116,7 @@
context 'when service_providers.yml has a remote setting' do
before do
location = 'https://raw.githubusercontent.com/18F/identity-idp/master/config/service_providers.yml'
- RemoteSetting.create(name: 'service_providers.yml', url:location,
+ RemoteSetting.create(name: 'service_providers.yml', url: location,
contents: "test:\n 'issuer1':\n friendly_name: 'name1'")
end
diff --git a/spec/services/twilio_service_spec.rb b/spec/services/twilio_service_spec.rb
index f1a72e9c66d..6246839f845 100644
--- a/spec/services/twilio_service_spec.rb
+++ b/spec/services/twilio_service_spec.rb
@@ -24,7 +24,7 @@
SmsOtpSenderJob.perform_now(
code: '1234',
- phone: '555-5555',
+ phone: '17035551212',
otp_created_at: Time.zone.now.to_s
)
diff --git a/spec/services/usps_confirmation_entry_spec.rb b/spec/services/usps_confirmation_entry_spec.rb
deleted file mode 100644
index 2b4d7de916b..00000000000
--- a/spec/services/usps_confirmation_entry_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'rails_helper'
-
-describe UspsConfirmationEntry do
- subject do
- described_class.new_from_hash(
- first_name: 'Some',
- last_name: 'One',
- otp: 123
- )
- end
-
- describe '#encrypted' do
- it 'encrypts' do
- encrypted_entry = subject.encrypted
-
- expect(encrypted_entry).to_not match 'Some'
- end
- end
-
- describe '#new_from_encrypted' do
- it 'round-trips entry' do
- encrypted_entry = subject.encrypted
- plain_entry = described_class.new_from_encrypted(encrypted_entry)
-
- expect(plain_entry).to eq subject
- end
- end
-end
diff --git a/spec/services/usps_confirmation_maker_spec.rb b/spec/services/usps_confirmation_maker_spec.rb
index bcbac173469..b62e2a38957 100644
--- a/spec/services/usps_confirmation_maker_spec.rb
+++ b/spec/services/usps_confirmation_maker_spec.rb
@@ -32,7 +32,8 @@
expect { subject.perform }.to change { UspsConfirmation.count }.from(0).to(1)
usps_confirmation = UspsConfirmation.first
- expect(usps_confirmation.decrypted_entry.to_h).to eq decrypted_attributes
+ entry_hash = usps_confirmation.entry
+ expect(entry_hash).to eq decrypted_attributes
end
it 'should create a UspsConfrimationCode with the profile and the encrypted OTP' do
diff --git a/spec/services/usps_confirmation_spec.rb b/spec/services/usps_confirmation_spec.rb
new file mode 100644
index 00000000000..cf20430d66a
--- /dev/null
+++ b/spec/services/usps_confirmation_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+describe UspsConfirmation do
+ let(:attributes) do
+ {
+ first_name: 'Homer',
+ last_name: 'Simpson',
+ ssn: '123-456-7890',
+ }
+ end
+ let(:encryptor) { Encryption::Encryptors::SessionEncryptor.new }
+
+ subject { UspsConfirmation.create!(entry: attributes) }
+
+ describe '#entry' do
+ it 'stores the entry as an encrypted json string' do
+ # Since the encryption is different every time, we'll just make sure this
+ # is some non-empty string thats NOT the json version of the attributes.
+ expect(subject[:entry]).to be_a(String)
+ expect(subject[:entry]).not_to be_empty
+ expect(subject[:entry]).not_to eq(attributes.to_json)
+ expect(parse(subject[:entry])).to eq(attributes)
+ end
+
+ it 'retrieves the entry as an unencrypted hash with symbolized keys' do
+ expect(subject.entry).to eq(attributes)
+ end
+ end
+
+ def parse(json)
+ JSON.parse(encryptor.decrypt(json), symbolize_names: true)
+ end
+end
diff --git a/spec/services/usps_exporter_spec.rb b/spec/services/usps_exporter_spec.rb
index de21baf1148..9596adc1450 100644
--- a/spec/services/usps_exporter_spec.rb
+++ b/spec/services/usps_exporter_spec.rb
@@ -36,12 +36,6 @@
]
values.join('|')
end
- let(:file_encryptor) do
- FileEncryptor.new(
- Rails.root.join('keys', 'equifax_gpg.pub.bin'),
- Figaro.env.equifax_gpg_email
- )
- end
subject { described_class.new(export_file.path) }
@@ -61,19 +55,12 @@
confirmation_maker.perform
end
- it 'creates encrypted file' do
+ it 'creates plain text file' do
subject.run
psv_contents = export_file.read
- expect(psv_contents).to_not eq("01|1\r\n#{psv_row_contents}\r\n")
-
- decrypted_contents = file_encryptor.decrypt(
- Figaro.env.equifax_development_example_gpg_passphrase,
- export_file.path
- )
-
- expect(decrypted_contents).to eq("01|1\r\n#{psv_row_contents}\r\n")
+ expect(psv_contents).to eq("01|1\r\n#{psv_row_contents}\r\n")
end
it 'clears entries after creating file' do
@@ -83,15 +70,5 @@
expect(UspsConfirmation.count).to eq 0
end
-
- it 'does not clear entries when GPG encrypting fails for some reason' do
- expect(Figaro.env).to receive(:equifax_gpg_email).and_return('wrong@email.com')
-
- original_count = UspsConfirmation.count
-
- expect { subject.run }.to raise_error(FileEncryptor::EncryptionError)
-
- expect(UspsConfirmation.count).to eq(original_count)
- end
end
end
diff --git a/spec/services/usps_uploader_spec.rb b/spec/services/usps_uploader_spec.rb
index b5f48ef530c..22c68e9f5e1 100644
--- a/spec/services/usps_uploader_spec.rb
+++ b/spec/services/usps_uploader_spec.rb
@@ -10,17 +10,17 @@
before do
sftp_options = [
- Figaro.env.equifax_sftp_host,
- Figaro.env.equifax_sftp_username,
- { key_data: [RequestKeyManager.equifax_ssh_key.to_pem] },
+ Figaro.env.usps_upload_sftp_host,
+ Figaro.env.usps_upload_sftp_username,
+ { password: Figaro.env.usps_upload_sftp_password },
]
expect(Net::SFTP).to receive(:start).
with(*sftp_options).and_yield(sftp_connection)
end
- it 'creates a PGP-encrypted file and uploads it via SFTP and deletes it after' do
+ it 'creates a file, uploads it via SFTP, and deletes it after' do
expect(sftp_connection).to receive(:upload!).
- with(uploader.local_path.to_s, File.join(Figaro.env.equifax_sftp_directory, 'batch.pgp'))
+ with(uploader.local_path.to_s, upload_folder)
run
@@ -36,4 +36,8 @@
expect(File.exist?(uploader.local_path)).to eq(true)
end
end
+
+ def upload_folder
+ File.join(Figaro.env.usps_upload_sftp_directory, 'batch.psv')
+ end
end
diff --git a/spec/support/fake_adapter.rb b/spec/support/fake_adapter.rb
new file mode 100644
index 00000000000..f721126e560
--- /dev/null
+++ b/spec/support/fake_adapter.rb
@@ -0,0 +1,24 @@
+module FakeAdapter
+ def self.post(_endpoint, _params)
+ SuccessResponse.new
+ end
+
+ class SuccessResponse
+ def success?
+ true
+ end
+ end
+
+ class ErrorResponse
+ def success?
+ false
+ end
+
+ def response_body
+ {
+ error_code: '60033',
+ message: 'Invalid number',
+ }.to_json
+ end
+ end
+end
diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb
index 74c5a877e9e..12c28fd8517 100644
--- a/spec/support/features/idv_helper.rb
+++ b/spec/support/features/idv_helper.rb
@@ -57,7 +57,7 @@ def fill_out_phone_form_ok(phone = '415-555-0199')
end
def fill_out_phone_form_fail
- fill_in :idv_phone_form_phone, with: '(555) 555-5555'
+ fill_in :idv_phone_form_phone, with: '(703) 555-5555'
end
def click_idv_continue
diff --git a/spec/support/features/idv_step_helper.rb b/spec/support/features/idv_step_helper.rb
index fb6484a75a1..6ddb4c0880f 100644
--- a/spec/support/features/idv_step_helper.rb
+++ b/spec/support/features/idv_step_helper.rb
@@ -48,7 +48,7 @@ def complete_idv_steps_before_usps_step(user = user_with_2fa)
def complete_idv_steps_before_phone_otp_delivery_selection_step(user = user_with_2fa)
complete_idv_steps_before_phone_step(user)
- fill_out_phone_form_ok('2341230638')
+ fill_out_phone_form_ok('2342255432')
click_idv_continue
end
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index 96672d210e2..94e7c921ebf 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -102,12 +102,12 @@ def sign_in_and_2fa_user(user = user_with_2fa)
end
def user_with_2fa
- create(:user, :signed_up, phone: '+1 (555) 555-0000', password: VALID_PASSWORD)
+ create(:user, :signed_up, phone: '+1 202-555-1212', password: VALID_PASSWORD)
end
def user_with_piv_cac
create(:user, :signed_up, :with_piv_or_cac,
- phone: '+1 (555) 555-0000',
+ phone: '+1 (703) 555-0000',
password: VALID_PASSWORD)
end
diff --git a/spec/support/idv_examples/failed_idv_job.rb b/spec/support/idv_examples/failed_idv_job.rb
index e9efa5bb055..8319acd2101 100644
--- a/spec/support/idv_examples/failed_idv_job.rb
+++ b/spec/support/idv_examples/failed_idv_job.rb
@@ -82,7 +82,7 @@
end
end
- # rubocop:disable Lint/HandleExceptions
+ # rubocop:disable Lint/HandleExceptions, Style/RedundantBegin
def stub_idv_job_to_raise_error_in_background(idv_job_class)
allow(Idv::Agent).to receive(:new).and_raise('this is a test error')
allow(idv_job_class).to receive(:perform_now).and_wrap_original do |perform_now, *args|
@@ -93,7 +93,7 @@ def stub_idv_job_to_raise_error_in_background(idv_job_class)
end
end
end
- # rubocop:enable Lint/HandleExceptions
+ # rubocop:enable Lint/HandleExceptions, Style/RedundantBegin
def stub_idv_job_to_timeout_in_background(idv_job_class)
allow(idv_job_class).to receive(:perform_now)
diff --git a/spec/support/key_rotation_helper.rb b/spec/support/key_rotation_helper.rb
index 8d66cc4c675..93240346b2e 100644
--- a/spec/support/key_rotation_helper.rb
+++ b/spec/support/key_rotation_helper.rb
@@ -5,10 +5,10 @@ def rotate_hmac_key
allow(env).to receive(:hmac_fingerprinter_key_queue).and_return(
"[\"#{old_hmac_key}\"]"
)
- allow(env).to receive(:hmac_fingerprinter_key).and_return('a-new-key')
+ allow(env).to receive(:hmac_fingerprinter_key).and_return('4' * 32)
end
- def rotate_attribute_encryption_key(new_key = 'a-new-key', new_cost = '4000$8$2$')
+ def rotate_attribute_encryption_key(new_key = '4' * 32, new_cost = '4000$8$2$')
env = Figaro.env
old_key = env.attribute_encryption_key
old_cost = env.attribute_cost
@@ -33,6 +33,6 @@ def rotate_attribute_encryption_key_with_invalid_queue
allow(env).to receive(:attribute_encryption_key_queue).and_return(
[{ key: 'key-that-was-never-used-in-the-past', cost: '4000$8$2$' }].to_json
)
- allow(env).to receive(:attribute_encryption_key).and_return('a-new-key')
+ allow(env).to receive(:attribute_encryption_key).and_return('4' * 32)
end
end
diff --git a/spec/support/saml_response_doc.rb b/spec/support/saml_response_doc.rb
index 12fb881d149..74067f5cbf4 100644
--- a/spec/support/saml_response_doc.rb
+++ b/spec/support/saml_response_doc.rb
@@ -34,7 +34,7 @@ def input_id
def raw_xml_response
if @test_type == 'feature'
xml_response
- elsif @response.body =~ //
+ elsif @response.body.match?(//)
html_response
else
@response.body
@@ -53,7 +53,7 @@ def saml_document
end
def response_doc
- if raw_xml_response =~ /EncryptedData/
+ if raw_xml_response.match?(/EncryptedData/)
@original_encrypted = true
Nokogiri::XML(
OneLogin::RubySaml::Response.new(
diff --git a/spec/support/shared_examples/remember_device.rb b/spec/support/shared_examples/remember_device.rb
index 25f1d717911..24c78a0b7c1 100644
--- a/spec/support/shared_examples/remember_device.rb
+++ b/spec/support/shared_examples/remember_device.rb
@@ -25,7 +25,7 @@
sign_in_user(user)
visit manage_phone_path
- fill_in 'user_phone_form_phone', with: '5551230000'
+ fill_in 'user_phone_form_phone', with: '7032231000'
click_button t('forms.buttons.submit.confirm_change')
click_submit_default
first(:link, t('links.sign_out')).click
diff --git a/spec/support/shared_examples_for_otp_delivery_preference_validation.rb b/spec/support/shared_examples_for_otp_delivery_preference_validation.rb
index ad2964464f6..42d041000ce 100644
--- a/spec/support/shared_examples_for_otp_delivery_preference_validation.rb
+++ b/spec/support/shared_examples_for_otp_delivery_preference_validation.rb
@@ -1,5 +1,5 @@
shared_examples 'an otp delivery preference form' do
- let(:phone) { '+1 (555) 555-5000' }
+ let(:phone) { '+1 (703) 555-5000' }
let(:params) do
{
phone: phone,
diff --git a/spec/support/shared_examples_for_phone_validation.rb b/spec/support/shared_examples_for_phone_validation.rb
index a8c3ffc386b..f77c04d4402 100644
--- a/spec/support/shared_examples_for_phone_validation.rb
+++ b/spec/support/shared_examples_for_phone_validation.rb
@@ -1,8 +1,6 @@
require 'shoulda/matchers'
shared_examples 'a phone form' do
- include Shoulda::Matchers::ActiveModel
-
describe 'phone presence validation' do
it 'is invalid when phone is blank' do
params[:phone] = ''
@@ -12,32 +10,6 @@
end
end
- describe 'phone validation' do
- it 'uses the phony_rails gem' do
- phone_validator = subject._validators.values.flatten.
- detect { |v| v.class == PhonyPlausibleValidator }
-
- expect(phone_validator.options[:presence]).to eq(true)
- expect(phone_validator.options[:message]).to eq(:improbable_phone)
- expect(phone_validator.options).to include(:international_code)
- end
-
- it do
- should validate_inclusion_of(:international_code).
- in_array(PhoneNumberCapabilities::INTERNATIONAL_CODES.keys)
- end
-
- it 'validates that the number matches the requested international code' do
- params[:phone] = '123 123 1234'
- params[:international_code] = 'MA'
- result = subject.submit(params)
-
- expect(result).to be_kind_of(FormResponse)
- expect(result.success?).to eq(false)
- expect(result.errors).to include(:phone)
- end
- end
-
describe 'phone uniqueness' do
context 'when phone is already taken' do
it 'is valid' do
@@ -63,7 +35,7 @@
context 'when phone is same as current user' do
it 'is valid' do
- user.phone = '+1 (555) 500-5000'
+ user.phone = '+1 (703) 500-5000'
params[:phone] = user.phone
result = subject.submit(params)
@@ -75,10 +47,10 @@
describe '#submit' do
it 'formats the phone before assigning it' do
- params[:phone] = '703-555-1212'
+ params[:phone] = '(703) 555-1212'
subject.submit(params)
- expect(subject.phone).to eq '+1 (703) 555-1212'
+ expect(subject.phone).to eq '+1 703-555-1212'
end
end
end
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
new file mode 100644
index 00000000000..f7f48780e14
--- /dev/null
+++ b/spec/views/account_reset/confirm_delete_account/show.html.slim_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+describe 'account_reset/confirm_delete_account/show.html.slim' do
+ before do
+ allow(view).to receive(:email).and_return('foo@bar.com')
+ end
+
+ it 'has a localized title' do
+ expect(view).to receive(:title).with(t('account_reset.confirm_delete_account.title'))
+
+ render
+ end
+
+ it 'contains the user email' do
+ email = 'foo@bar.com'
+ session[:email] = email
+
+ render
+
+ expect(rendered).to have_content(email)
+ end
+
+ 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
+ )
+ end
+end
diff --git a/spec/views/account_reset/confirm_request/show.html.slim_spec.rb b/spec/views/account_reset/confirm_request/show.html.slim_spec.rb
new file mode 100644
index 00000000000..471a851cd0d
--- /dev/null
+++ b/spec/views/account_reset/confirm_request/show.html.slim_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+describe 'account_reset/confirm_request/show.html.slim' do
+ before do
+ allow(view).to receive(:email).and_return('foo@bar.com')
+ end
+
+ it 'has a localized title' do
+ expect(view).to receive(:title).with(t('account_reset.confirm_request.check_your_email'))
+
+ render
+ end
+
+ it 'contains the user email' do
+ email = 'foo@bar.com'
+ session[:email] = email
+
+ render
+
+ expect(rendered).to have_content(email)
+ 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
new file mode 100644
index 00000000000..6fe0957933e
--- /dev/null
+++ b/spec/views/account_reset/delete_account/show.html.slim_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+describe 'account_reset/delete_account/show.html.slim' do
+ before do
+ allow(view).to receive(:email).and_return('foo@bar.com')
+ end
+
+ it 'has a localized title' do
+ expect(view).to receive(:title).with(t('account_reset.delete_account.title'))
+
+ render
+ end
+
+ it 'has button to delete' do
+
+ render
+ expect(rendered).to have_button t('account_reset.delete_account.delete_button')
+ end
+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
new file mode 100644
index 00000000000..d68284b8311
--- /dev/null
+++ b/spec/views/account_reset/request/show.html.slim_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+describe 'account_reset/request/show.html.slim' do
+ it 'has a localized title' do
+ expect(view).to receive(:title).with(t('account_reset.request.title'))
+
+ render
+ end
+
+ it 'has button to delete' do
+
+ render
+ expect(rendered).to have_button t('account_reset.request.yes_continue')
+ end
+end
diff --git a/spec/views/idv/review/new.html.slim_spec.rb b/spec/views/idv/review/new.html.slim_spec.rb
index e61667b654c..52fa0b5e440 100644
--- a/spec/views/idv/review/new.html.slim_spec.rb
+++ b/spec/views/idv/review/new.html.slim_spec.rb
@@ -29,7 +29,7 @@
expect(rendered).to have_content('MO')
expect(rendered).to have_content('12345')
expect(rendered).to have_content('666-66-1234')
- expect(rendered).to have_content('+1 (213) 555-0000')
+ expect(rendered).to have_content('+1 213-555-0000')
expect(rendered).to have_content('March 29, 1972')
end