diff --git a/app/assets/images/come-back.svg b/app/assets/images/come-back.svg new file mode 100644 index 00000000000..d528f54cdcb --- /dev/null +++ b/app/assets/images/come-back.svg @@ -0,0 +1 @@ +come-back \ No newline at end of file diff --git a/app/assets/images/spinner.gif b/app/assets/images/spinner.gif index 913ce4c9165..8646d68e705 100644 Binary files a/app/assets/images/spinner.gif and b/app/assets/images/spinner.gif differ diff --git a/app/assets/images/spinner@2x.gif b/app/assets/images/spinner@2x.gif new file mode 100644 index 00000000000..0cd600cca68 Binary files /dev/null and b/app/assets/images/spinner@2x.gif differ diff --git a/app/assets/javascripts/app/form-field-format.js b/app/assets/javascripts/app/form-field-format.js index f00ec070241..b03ee377608 100644 --- a/app/assets/javascripts/app/form-field-format.js +++ b/app/assets/javascripts/app/form-field-format.js @@ -3,6 +3,7 @@ import DateFormatter from './modules/date-formatter'; import InternationalPhoneFormatter from './modules/international-phone-formatter'; import NumericFormatter from './modules/numeric-formatter'; import PersonalKeyFormatter from './modules/personal-key-formatter'; +import USPhoneFormatter from './modules/us-phone-formatter'; import ZipCodeFormatter from './modules/zip-code-formatter'; @@ -15,6 +16,7 @@ function formatForm() { ['.mfa', new NumericFormatter()], ['.mortgage', new NumericFormatter()], ['.phone', new InternationalPhoneFormatter()], + ['.us-phone', new USPhoneFormatter()], ['.personal-key', new PersonalKeyFormatter()], ['.ssn', new SocialSecurityNumberFormatter()], ['.zipcode', new ZipCodeFormatter()], diff --git a/app/assets/javascripts/app/modules/us-phone-formatter.js b/app/assets/javascripts/app/modules/us-phone-formatter.js new file mode 100644 index 00000000000..062cd827e11 --- /dev/null +++ b/app/assets/javascripts/app/modules/us-phone-formatter.js @@ -0,0 +1,16 @@ +import { PhoneFormatter } from 'field-kit'; + +class USPhoneFormatter extends PhoneFormatter { + isChangeValid(change, error) { + const match = change.proposed.text.match(/^\+(\d?)/); + if (match && match[1] === '') { + change.proposed.text = '+1'; + change.proposed.selectedRange.start = 4; + } else if (match && match[1] !== '1') { + return false; + } + return super.isChangeValid(change, error); + } +} + +export default USPhoneFormatter; diff --git a/app/assets/javascripts/app/phone-internationalization.js b/app/assets/javascripts/app/phone-internationalization.js index af3e4724a33..fa66449c535 100644 --- a/app/assets/javascripts/app/phone-internationalization.js +++ b/app/assets/javascripts/app/phone-internationalization.js @@ -6,7 +6,7 @@ const I18n = window.LoginGov.I18n; const phoneFormatter = new PhoneFormatter(); const getPhoneUnsupportedAreaCodeCountry = (areaCode) => { - const form = document.querySelector('#new_two_factor_setup_form'); + const form = document.querySelector('[data-international-phone-form]'); const phoneUnsupportedAreaCodes = JSON.parse(form.dataset.unsupportedAreaCodes); return phoneUnsupportedAreaCodes[areaCode]; }; @@ -52,8 +52,8 @@ const unsupportedPhoneOTPDeliveryWarningMessage = (phone) => { }; const updateOTPDeliveryMethods = () => { - const phoneRadio = document.querySelector('#two_factor_setup_form_otp_delivery_preference_voice'); - const smsRadio = document.querySelector('#two_factor_setup_form_otp_delivery_preference_sms'); + const phoneRadio = document.querySelector('[data-international-phone-form] .otp_delivery_preference_voice'); + const smsRadio = document.querySelector('[data-international-phone-form] .otp_delivery_preference_sms'); if (!phoneRadio || !smsRadio) { return; diff --git a/app/assets/stylesheets/components/_loading.scss b/app/assets/stylesheets/components/_loading.scss index 9a2e669ae3a..4323690d219 100644 --- a/app/assets/stylesheets/components/_loading.scss +++ b/app/assets/stylesheets/components/_loading.scss @@ -1,4 +1,4 @@ .loading-spinner { margin: auto; - width: 100px; + width: 144px; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c623af396b6..d0706a151df 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -69,7 +69,11 @@ def redirect_on_timeout return unless params[:timeout] flash[:notice] = t('notices.session_cleared', minutes: Figaro.env.session_timeout_in_minutes) - redirect_to url_for(params.except(:timeout)) + redirect_to url_for(permitted_timeout_params) + end + + def permitted_timeout_params + params.permit(:request_id) end def current_sp diff --git a/app/controllers/concerns/two_factor_authenticatable.rb b/app/controllers/concerns/two_factor_authenticatable.rb index 02f005fd2a3..1fa1b566c70 100644 --- a/app/controllers/concerns/two_factor_authenticatable.rb +++ b/app/controllers/concerns/two_factor_authenticatable.rb @@ -187,7 +187,7 @@ def reset_otp_session_data def after_otp_verification_confirmation_path if idv_context? - verify_confirmations_path + verify_review_path elsif after_otp_action_required? after_otp_action_path else @@ -231,9 +231,11 @@ def unconfirmed_phone? user_session[:unconfirmed_phone] && idv_or_confirmation_context? end + # rubocop:disable MethodLength def phone_view_data { confirmation_for_phone_change: confirmation_for_phone_change?, + confirmation_for_idv: idv_context?, phone_number: display_phone_to_deliver_to, code_value: direct_otp_code, otp_delivery_preference: two_factor_authentication_method, @@ -243,6 +245,7 @@ def phone_view_data totp_enabled: current_user.totp_enabled?, }.merge(generic_data) end + # rubocop:enable MethodLength def authenticator_view_data { diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb index 7d8c2c05be3..d6723c4a15d 100644 --- a/app/controllers/users/phones_controller.rb +++ b/app/controllers/users/phones_controller.rb @@ -5,13 +5,13 @@ class PhonesController < ReauthnRequiredController before_action :confirm_two_factor_authenticated def edit - @update_user_phone_form = UpdateUserPhoneForm.new(current_user) + @user_phone_form = UserPhoneForm.new(current_user) end def update - @update_user_phone_form = UpdateUserPhoneForm.new(current_user) + @user_phone_form = UserPhoneForm.new(current_user) - if @update_user_phone_form.submit(user_params).success? + if @user_phone_form.submit(user_params).success? process_updates bypass_sign_in current_user else @@ -22,14 +22,14 @@ def update private def user_params - params.require(:update_user_phone_form).permit(:phone, :international_code) + params.require(:user_phone_form).permit(:phone, :international_code, :otp_delivery_preference) end def process_updates - if @update_user_phone_form.phone_changed? + if @user_phone_form.phone_changed? analytics.track_event(Analytics::PHONE_CHANGE_REQUESTED) flash[:notice] = t('devise.registrations.phone_update_needs_confirmation') - prompt_to_confirm_phone(phone: @update_user_phone_form.phone) + prompt_to_confirm_phone(phone: @user_phone_form.phone) else redirect_to account_url end diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 5d21cf83494..bc02addb6ad 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -8,14 +8,13 @@ class TwoFactorAuthenticationSetupController < ApplicationController skip_before_action :handle_two_factor_authentication def index - @two_factor_setup_form = TwoFactorSetupForm.new(current_user) - @unsupported_area_codes = PhoneNumberCapabilities::VOICE_UNSUPPORTED_US_AREA_CODES + @user_phone_form = UserPhoneForm.new(current_user) analytics.track_event(Analytics::USER_REGISTRATION_PHONE_SETUP_VISIT) end def set - @two_factor_setup_form = TwoFactorSetupForm.new(current_user) - result = @two_factor_setup_form.submit(params[:two_factor_setup_form]) + @user_phone_form = UserPhoneForm.new(current_user) + result = @user_phone_form.submit(params[:user_phone_form]) analytics.track_event(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result.to_h) @@ -37,7 +36,7 @@ def authorize_otp_setup end def process_valid_form - prompt_to_confirm_phone(phone: @two_factor_setup_form.phone) + prompt_to_confirm_phone(phone: @user_phone_form.phone) end end end diff --git a/app/controllers/verify/come_back_later_controller.rb b/app/controllers/verify/come_back_later_controller.rb new file mode 100644 index 00000000000..1aab4f04b13 --- /dev/null +++ b/app/controllers/verify/come_back_later_controller.rb @@ -0,0 +1,20 @@ +module Verify + class ComeBackLaterController < ApplicationController + include IdvSession + + before_action :confirm_idv_session_completed + before_action :confirm_usps_verification_method_chosen + + def show; end + + private + + def confirm_idv_session_completed + redirect_to account_path if idv_session.profile.blank? + end + + def confirm_usps_verification_method_chosen + redirect_to account_path unless idv_session.address_verification_mechanism == 'usps' + end + end +end diff --git a/app/controllers/verify/confirmations_controller.rb b/app/controllers/verify/confirmations_controller.rb index a359a255956..b98393c244a 100644 --- a/app/controllers/verify/confirmations_controller.rb +++ b/app/controllers/verify/confirmations_controller.rb @@ -21,6 +21,8 @@ def update def next_step if session[:sp] && !pending_profile? sign_up_completed_url + elsif pending_profile? && idv_session.address_verification_mechanism == 'usps' + verify_come_back_later_path else after_sign_in_path_for(current_user) end diff --git a/app/controllers/verify/phone_controller.rb b/app/controllers/verify/phone_controller.rb index 888b08ac2b9..ca6f2bc11f1 100644 --- a/app/controllers/verify/phone_controller.rb +++ b/app/controllers/verify/phone_controller.rb @@ -2,6 +2,7 @@ module Verify class PhoneController < ApplicationController include IdvStepConcern include IdvFailureConcern + include PhoneConfirmation before_action :confirm_step_needed before_action :confirm_step_allowed @@ -31,7 +32,7 @@ def show increment_step_attempts if result.success? - redirect_to verify_review_url + redirect_to_next_step else render_failure render :new @@ -40,6 +41,24 @@ def show private + def redirect_to_next_step + if phone_confirmation_required? + prompt_to_confirm_phone(phone: idv_session.params[:phone], context: 'idv') + else + redirect_to verify_review_url + end + end + + def phone_confirmation_required? + normalized_phone = idv_session.params[:phone] + return false if normalized_phone.blank? + + formatted_phone = normalized_phone.phony_formatted( + format: :international, normalize: :US, spaces: ' ' + ) + formatted_phone != current_user.phone + end + def submit_idv_job SubmitIdvJob.new( vendor_validator_class: Idv::PhoneValidator, @@ -69,7 +88,7 @@ def step_params end def confirm_step_needed - redirect_to verify_review_path if idv_session.vendor_phone_confirmation == true + redirect_to_next_step if idv_session.vendor_phone_confirmation == true end def idv_form diff --git a/app/controllers/verify/review_controller.rb b/app/controllers/verify/review_controller.rb index b55e8584093..7a54267c832 100644 --- a/app/controllers/verify/review_controller.rb +++ b/app/controllers/verify/review_controller.rb @@ -4,6 +4,7 @@ class ReviewController < ApplicationController include PhoneConfirmation before_action :confirm_idv_steps_complete + before_action :confirm_idv_phone_confirmed before_action :confirm_current_password, only: [:create] def confirm_idv_steps_complete @@ -12,6 +13,16 @@ def confirm_idv_steps_complete return redirect_to(verify_address_path) unless idv_address_complete? end + def confirm_idv_phone_confirmed + return unless idv_session.address_verification_mechanism == 'phone' + return if idv_session.phone_confirmed? + + prompt_to_confirm_phone( + phone: idv_session.params[:phone], + context: 'idv' + ) + end + def confirm_current_password return if valid_password? @@ -34,7 +45,7 @@ def new def create init_profile - redirect_to_next_step + redirect_to verify_confirmations_path analytics.track_event(Analytics::IDV_REVIEW_COMPLETE) end @@ -68,27 +79,10 @@ def init_profile idv_session.cache_encrypted_pii(current_user.user_access_key) end - def redirect_to_next_step - if phone_confirmation_required? - prompt_to_confirm_phone(phone: idv_params[:phone], context: 'idv') - else - redirect_to verify_confirmations_path - end - end - def idv_params idv_session.params end - def phone_confirmation_required? - normalized_phone = idv_params[:phone] - return false if normalized_phone.blank? - - formatted_phone = PhoneFormatter.new.format(normalized_phone) - formatted_phone != current_user.phone && - idv_session.address_verification_mechanism == 'phone' - end - def valid_password? current_user.valid_password?(password) end diff --git a/app/controllers/verify/usps_controller.rb b/app/controllers/verify/usps_controller.rb index bc850e6c921..642810fbe3e 100644 --- a/app/controllers/verify/usps_controller.rb +++ b/app/controllers/verify/usps_controller.rb @@ -11,7 +11,12 @@ def index def create create_user_event(:usps_mail_sent, current_user) idv_session.address_verification_mechanism = :usps - redirect_to verify_review_url + + if current_user.decorate.needs_profile_usps_verification? + redirect_to account_path + else + redirect_to verify_review_url + end end def usps_mail_service diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb index f59e0b4a6bf..dd490e0c0b0 100644 --- a/app/forms/idv/phone_form.rb +++ b/app/forms/idv/phone_form.rb @@ -6,20 +6,17 @@ class PhoneForm attr_reader :idv_params, :user, :phone attr_accessor :international_code + validate :phone_has_us_country_code + def initialize(idv_params, user) @idv_params = idv_params @user = user - self.phone = (idv_params[:phone] || user.phone).phony_formatted( - format: :international, normalize: :US, spaces: ' ' - ) + self.phone = initial_phone_value(idv_params[:phone] || user.phone) self.international_code = PhoneFormatter::DEFAULT_COUNTRY end def submit(params) - submitted_phone = params[:phone] - - formatted_phone = PhoneFormatter.new.format(submitted_phone, country_code: international_code) - + formatted_phone = PhoneFormatter.new.format(params[:phone]) self.phone = formatted_phone success = valid? @@ -32,6 +29,21 @@ 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 + self.phone = formatted_phone + end + + def phone_has_us_country_code + country_code = Phonelib.parse(phone).country_code || '1' + return if country_code == '1' + + errors.add(:phone, :must_have_us_country_code) + end + def update_idv_params(phone) normalized_phone = phone.gsub(/\D/, '')[1..-1] idv_params[:phone] = normalized_phone diff --git a/app/forms/update_user_phone_form.rb b/app/forms/update_user_phone_form.rb deleted file mode 100644 index 16f06558ead..00000000000 --- a/app/forms/update_user_phone_form.rb +++ /dev/null @@ -1,43 +0,0 @@ -class UpdateUserPhoneForm - include ActiveModel::Model - include FormPhoneValidator - - attr_accessor :phone, :international_code - attr_reader :user - - def persisted? - true - end - - def initialize(user) - @user = user - self.phone = @user.phone - self.international_code = Phonelib.parse(phone).country || PhoneFormatter::DEFAULT_COUNTRY - end - - def submit(params) - self.phone = params[:phone] - self.international_code = params[:international_code] - - check_phone_change - - FormResponse.new(success: valid?, errors: errors.messages) - end - - def phone_changed? - phone_changed == true - end - - private - - attr_reader :phone_changed - - def check_phone_change - formatted_phone = PhoneFormatter.new.format(phone, country_code: international_code) - - return unless formatted_phone != @user.phone - - @phone_changed = true - self.phone = formatted_phone - end -end diff --git a/app/forms/two_factor_setup_form.rb b/app/forms/user_phone_form.rb similarity index 51% rename from app/forms/two_factor_setup_form.rb rename to app/forms/user_phone_form.rb index bc8a123dc8e..8a3c0632fd3 100644 --- a/app/forms/two_factor_setup_form.rb +++ b/app/forms/user_phone_form.rb @@ -1,34 +1,51 @@ -class TwoFactorSetupForm +class UserPhoneForm include ActiveModel::Model include FormPhoneValidator include OtpDeliveryPreferenceValidator - attr_accessor :phone, :international_code + attr_accessor :phone, :international_code, :otp_delivery_preference def initialize(user) - @user = user + self.user = user + self.phone = user.phone + self.international_code = Phonelib.parse(phone).country || PhoneFormatter::DEFAULT_COUNTRY + self.otp_delivery_preference = user.otp_delivery_preference end def submit(params) - process_phone_number_params(params) - self.otp_delivery_preference = params[:otp_delivery_preference] + ingest_submitted_params(params) - @success = valid? + success = valid? - update_otp_delivery_preference_for_user if success && otp_delivery_preference_changed? + self.phone = submitted_phone unless success + update_otp_delivery_preference_for_user if otp_delivery_preference_changed? && success FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes) end - def process_phone_number_params(params) - self.international_code = params[:international_code] - self.phone = PhoneFormatter.new.format(params[:phone], country_code: international_code) + def phone_changed? + user.phone != phone end private - attr_reader :success, :user - attr_accessor :otp_delivery_preference + attr_accessor :user, :submitted_phone + + def extra_analytics_attributes + { + otp_delivery_preference: otp_delivery_preference, + } + end + + def ingest_submitted_params(params) + self.international_code = params[:international_code] + self.submitted_phone = params[:phone] + self.phone = PhoneFormatter.new.format( + submitted_phone, + country_code: international_code + ) + self.otp_delivery_preference = params[:otp_delivery_preference] + end def otp_delivery_preference_changed? otp_delivery_preference != user.otp_delivery_preference @@ -38,10 +55,4 @@ 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 extra_analytics_attributes - { - otp_delivery_preference: otp_delivery_preference, - } - end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 73b8c819fed..8e2cc0d1081 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -77,6 +77,10 @@ def international_phone_codes end end + def unsupported_area_codes + PhoneNumberCapabilities::VOICE_UNSUPPORTED_US_AREA_CODES + end + private def international_phone_code_label(code_data) 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 49c729bc0cf..52d9de93bd9 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,8 @@ def fallback_links def cancel_link if confirmation_for_phone_change || reauthn account_path + elsif confirmation_for_idv + verify_cancel_path else sign_out_path end @@ -34,8 +36,9 @@ def cancel_link :phone_number, :unconfirmed_phone, :otp_delivery_preference, + :confirmation_for_phone_change, :voice_otp_delivery_unsupported, - :confirmation_for_phone_change + :confirmation_for_idv ) def phone_number_tag diff --git a/app/services/request_key_manager.rb b/app/services/request_key_manager.rb index a8467b63909..8115b9cae71 100644 --- a/app/services/request_key_manager.rb +++ b/app/services/request_key_manager.rb @@ -1,15 +1,21 @@ class RequestKeyManager - cattr_accessor :private_key do + def self.read_key_file(key_file, passphrase) OpenSSL::PKey::RSA.new( - File.read(Rails.root.join('keys', 'saml.key.enc')), - Figaro.env.saml_passphrase + File.read(key_file), + passphrase ) + rescue OpenSSL::PKey::RSAError + raise OpenSSL::PKey::RSAError, "Failed to load #{key_file.inspect}. Bad passphrase?" + end + private_class_method :read_key_file + + cattr_accessor :private_key do + key_file = Rails.root.join('keys', 'saml.key.enc') + read_key_file(key_file, Figaro.env.saml_passphrase) end cattr_accessor :equifax_ssh_key do - OpenSSL::PKey::RSA.new( - File.read(Rails.root.join('keys', 'equifax_rsa')), - Figaro.env.equifax_ssh_passphrase - ) + key_file = Rails.root.join('keys', 'equifax_rsa') + read_key_file(key_file, Figaro.env.equifax_ssh_passphrase) end end diff --git a/app/views/shared/_dap_analytics.html.erb b/app/views/shared/_dap_analytics.html.erb index 6ff51cd70ef..2d12d4a5801 100644 --- a/app/views/shared/_dap_analytics.html.erb +++ b/app/views/shared/_dap_analytics.html.erb @@ -1,4 +1,4 @@ -<%= t('notices.dap_html') %> + <% dap_source = 'https://dap.digitalgov.gov/Universal-Federated-Analytics-Min.js?agency=GSA' %> <%= nonced_javascript_tag({src: dap_source, async: true, id: '_fed_an_ua_tag'}) do %> <% end %> diff --git a/app/views/shared/refresh.html.slim b/app/views/shared/refresh.html.slim index 6c7ef0103ec..8bb1287a5e1 100644 --- a/app/views/shared/refresh.html.slim +++ b/app/views/shared/refresh.html.slim @@ -4,4 +4,8 @@ h2 = t('idv.messages.loading') .loading-spinner - img src="#{image_path('spinner.gif')}" height=100 width=100 alt="" + img { src="#{image_path('spinner.gif')}" + srcset="#{image_path('spinner@2x.gif')} 2x" + height="144" + width="144" + alt="" } diff --git a/app/views/users/phones/edit.html.slim b/app/views/users/phones/edit.html.slim index 73ba00505a9..74d02d0504f 100644 --- a/app/views/users/phones/edit.html.slim +++ b/app/views/users/phones/edit.html.slim @@ -2,16 +2,21 @@ h1.h3.my0 = t('headings.edit_info.phone') -= simple_form_for(@update_user_phone_form, - data: { unsupported_area_codes: @unsupported_area_codes, += simple_form_for(@user_phone_form, + html: { autocomplete: 'off', method: :put, role: 'form' }, + data: { unsupported_area_codes: unsupported_area_codes, international_phone_form: true }, - url: manage_phone_path, - html: { autocomplete: 'off', method: :put, role: 'form' }) do |f| + url: manage_phone_path) do |f| = f.input :international_code, collection: international_phone_codes, include_blank: false, input_html: { class: 'international-code' } + = f.label :phone + strong.left = t('account.index.phone') + span#otp_phone_label_info.ml1.italic + = t('devise.two_factor_authentication.otp_phone_label_info') = f.input :phone, as: :tel, required: true, input_html: { class: 'phone', value: nil }, - label: t('account.index.phone') + label: false + = render 'users/shared/otp_delivery_preference_selection' = f.button :submit, t('forms.buttons.submit.confirm_change'), class: 'mt2' = render 'shared/cancel', link: account_path diff --git a/app/views/users/shared/_otp_delivery_preference_selection.html.slim b/app/views/users/shared/_otp_delivery_preference_selection.html.slim new file mode 100644 index 00000000000..0d95b2d61f3 --- /dev/null +++ b/app/views/users/shared/_otp_delivery_preference_selection.html.slim @@ -0,0 +1,17 @@ +.mb3 + fieldset.m0.p0.border-none + legend.mb1.h4.serif.bold = t('devise.two_factor_authentication.otp_delivery_preference.title') + label.btn-border.col-12.sm-col-5.sm-mr2.mb2.sm-mb0 + .radio + = radio_button_tag 'user_phone_form[otp_delivery_preference]', :sms, true, + class: :otp_delivery_preference_sms + span.indicator + = t('devise.two_factor_authentication.otp_delivery_preference.sms') + label.btn-border.col-12.sm-col-5 + .radio + = radio_button_tag 'user_phone_form[otp_delivery_preference]', :voice, false, + class: :otp_delivery_preference_voice + span.indicator + = t('devise.two_factor_authentication.otp_delivery_preference.voice') + p#otp_delivery_preference_instruction.mt1.mb0 + = t('devise.two_factor_authentication.otp_delivery_preference.instruction') diff --git a/app/views/users/two_factor_authentication_setup/index.html.slim b/app/views/users/two_factor_authentication_setup/index.html.slim index 2a86ed90b15..fcd3d0779df 100644 --- a/app/views/users/two_factor_authentication_setup/index.html.slim +++ b/app/views/users/two_factor_authentication_setup/index.html.slim @@ -3,9 +3,9 @@ h1.h3.my0 = t('devise.two_factor_authentication.two_factor_setup') p.mt-tiny.mb0 = t('devise.two_factor_authentication.otp_setup_html') -= simple_form_for(@two_factor_setup_form, += simple_form_for(@user_phone_form, html: { autocomplete: 'off', role: 'form' }, - data: { unsupported_area_codes: @unsupported_area_codes, + data: { unsupported_area_codes: unsupported_area_codes, international_phone_form: true }, method: :patch, url: phone_setup_path) do |f| @@ -21,21 +21,7 @@ p.mt-tiny.mb0 = t('devise.two_factor_authentication.otp_phone_label_info') = f.input :phone, as: :tel, label: false, required: true, input_html: { class: 'phone sm-col-8 mb4' } - .mb3 - fieldset.m0.p0.border-none - legend.mb1.h4.serif.bold = t('devise.two_factor_authentication.otp_delivery_preference.title') - label.btn-border.col-12.sm-col-5.sm-mr2.mb2.sm-mb0 - .radio - = radio_button_tag 'two_factor_setup_form[otp_delivery_preference]', :sms, checked: true - span.indicator - = t('devise.two_factor_authentication.otp_delivery_preference.sms') - label.btn-border.col-12.sm-col-5 - .radio - = radio_button_tag 'two_factor_setup_form[otp_delivery_preference]', :voice - span.indicator - = t('devise.two_factor_authentication.otp_delivery_preference.voice') - p#otp_delivery_preference_instruction.mt1.mb0 - = t('devise.two_factor_authentication.otp_delivery_preference.instruction') + = render 'users/shared/otp_delivery_preference_selection' = f.button :submit, t('forms.buttons.send_security_code') = render 'shared/cancel', link: destroy_user_session_path diff --git a/app/views/verify/come_back_later/show.html.slim b/app/views/verify/come_back_later/show.html.slim new file mode 100644 index 00000000000..840f44fdf92 --- /dev/null +++ b/app/views/verify/come_back_later/show.html.slim @@ -0,0 +1,13 @@ += image_tag(asset_url('come-back.svg'), + size: '140', alt: t('idv.titles.come_back_later'), class: 'block mx-auto') +h2.center = t('idv.titles.come_back_later') +p = t('idv.messages.come_back_later') +.center + - if decorated_session.sp_return_url.present? + = link_to(t('idv.buttons.return_to_sp', sp: decorated_session.sp_name), + decorated_session.sp_return_url, + class: 'btn btn-bordered border-green bw2 center rounded-lg') + - else + = link_to(t('idv.buttons.return_to_account'), + account_path, + class: 'btn btn-bordered border-green bw2 center rounded-lg') diff --git a/app/views/verify/phone/new.html.slim b/app/views/verify/phone/new.html.slim index fc594dfa05c..39dd10e522d 100644 --- a/app/views/verify/phone/new.html.slim +++ b/app/views/verify/phone/new.html.slim @@ -12,6 +12,8 @@ p.mt-tiny.mb2 = t('idv.messages.phone.intro') = t('idv.messages.phone.in_your_name') li = t('idv.messages.phone.prepaid') + li + = t('idv.messages.phone.us_country_code') em = t('idv.messages.phone.same_as_2fa') @@ -22,7 +24,7 @@ em span.ml1 em = t('idv.form.phone_label_aside') - = f.input :phone, required: true, input_html: { class: 'phone' }, label: false, + = f.input :phone, required: true, input_html: { class: 'us-phone' }, label: false, wrapper_html: { class: 'inline-block mr2' } = f.button :submit, t('forms.buttons.continue') diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 38a86bf6719..13d14c83684 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -19,6 +19,8 @@ en: improbable_phone: Invalid phone number. Please make sure you enter a valid phone number. missing_field: Please fill in this field. + must_have_us_country_code: Invalid phone number. Please make sure you enter + a phone number with a U.S. country code. no_password_reset_profile: No profile has been recently deactivated due to a password reset no_pending_profile: No profile is waiting for verification diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index bdac55909c4..aaf5b701dfa 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -18,6 +18,7 @@ es: format_mismatch: Por favor, use el formato solicitado. improbable_phone: NOT TRANSLATED YET missing_field: Por favor, rellene este campo. + must_have_us_country_code: NOT TRANSLATED YET no_password_reset_profile: Ningún perfil ha sido desactivado recientemente por un restablecimiento de contraseña. no_pending_profile: Ningún perfil está esperando verificación diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index 798dbd4f92c..5ce5df6cfc5 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -21,6 +21,7 @@ fr: format_mismatch: Veuillez vous assurer de respecter le format requis. improbable_phone: NOT TRANSLATED YET missing_field: Veuillez remplir ce champ. + must_have_us_country_code: NOT TRANSLATED YET no_password_reset_profile: Aucun profil récemment désactivé en raison d'une réinitialisation de mot de passe no_pending_profile: Aucun profil en attente de vérification diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 99ad1b0168d..b413ebc3505 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -10,6 +10,8 @@ en: mail: resend: Send another letter send: Send a letter + return_to_account: Return to account + return_to_sp: Return to %{sp} cancel: modal_header: Are you sure you want to cancel? warning_header: If you cancel now @@ -99,6 +101,8 @@ en: some basic personal information as well as some financial information from you to complete this process. If you don’t have the information we need at this time you can continue later. + come_back_later: Once your letter arrives, sign into login.gov and enter your + verification code when prompted. confirm: You have encrypted your verified data dupe_ssn1: If you are getting this error, you may have already created and verified an account, but with a different email address. @@ -134,6 +138,7 @@ en: prepaid: on a contract, not prepaid same_as_2fa: This phone number can be the same one you used to set up your one-time password as long as it meets the criteria above. + us_country_code: have a U.S. country code to accept phone calls review: financial_info: Where is my financial account information? info_verified_html: We found records matching your %{phone_message} @@ -189,6 +194,7 @@ en: titles: activated: Your identity has already been verified cancel: We cannot verify your identity + come_back_later: Come back soon complete: Identity verification complete dupe: 'Error: An account may already exist' expectations: Next, help us identify you diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index e4c80d91f3d..b21a8e6f7a4 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -10,6 +10,8 @@ es: mail: resend: Enviar otra carta send: Enviar una carta + return_to_account: NOT TRANSLATED YET + return_to_sp: NOT TRANSLATED YET cancel: modal_header: "¿Está seguro que desea cancelar?" warning_header: Si usted cancela ahora @@ -101,6 +103,7 @@ es: información personal básica, así como su información financiera para completar este proceso. Si no tiene la información que necesitamos en este momento, puede continuar más tarde. + come_back_later: NOT TRANSLATED YET confirm: Usted ha encriptado sus datos verificados. dupe_ssn1: Si recibe este error, es posible que ya haya creado y verificado una cuenta, pero con un email diferente. @@ -138,6 +141,7 @@ es: same_as_2fa: Este número de teléfono puede ser el mismo que utilizó para configurar su contraseña de un uso único, siempre y cuando cumpla con los criterios anteriores. + us_country_code: NOT TRANSLATED YET review: financial_info: "¿Dónde está la información de mi cuenta financiera?" info_verified_html: Encontramos registros que coinciden con su %{teléfono_mensaje} @@ -196,6 +200,7 @@ es: titles: activated: Su identidad ha sido verificada. cancel: No podemos verificar su identidad. + come_back_later: NOT TRANSLATED YET complete: Verificación de identidad completa dupe: 'Error: Una cuenta puede que ya exista.' expectations: A continuación, ayúdenos a identificarlo. diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 4391f11b268..923da86d2c8 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -10,6 +10,8 @@ fr: mail: resend: Envoyer une autre lettre send: Envoyer une lettre + return_to_account: NOT TRANSLATED YET + return_to_sp: NOT TRANSLATED YET cancel: modal_header: Souhaitez-vous vraiment annuler? warning_header: Si vous annulez maintenant @@ -106,6 +108,7 @@ fr: financières pour compléter ce processus. Si vous n'avez pas cette information sous la main, vous pouvez continuer plus tard. confirm: Vous avez crypté vos données vérifiées + come_back_later: NOT TRANSLATED YET dupe_ssn1: Si vous recevez ce message d'erreur, il est possible que vous ayez déjà créé et vérifié un compte, mais avec une adresse courriel différente. dupe_ssn2_html: Veuillez %{link} avec l'adresse courriel que vous avez utilisée @@ -147,6 +150,7 @@ fr: same_as_2fa: Ce numéro de téléphone peut être le même que celui que vous utilisez pour configurer votre mot de passe à usage unique, tant et aussi longtemps qu'il respecte les critères mentionnés plus haut. + us_country_code: NOT TRANSLATED YET review: financial_info: Où se trouve l'information sur mon compte bancaire? info_verified_html: Nous avons trouvé des données qui correspondent à votre @@ -208,6 +212,7 @@ fr: titles: activated: Votre identité a déjà été vérifiée cancel: Nous ne pouvons pas vérifier votre identité + come_back_later: NOT TRANSLATED YET complete: Vérification de votre identité complétée dupe: 'Erreur: un compte existe déjà' expectations: Ensuite, aidez-vous à vous identifier diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml index 98ebefedb49..7268fd6b6da 100644 --- a/config/locales/notices/en.yml +++ b/config/locales/notices/en.yml @@ -2,8 +2,8 @@ en: notices: account_reactivation: Great! You have your personal key. - dap_html: " " + dap_participation: We participate in the US government’s analytics program. See + the data at analytics.usa.gov. forgot_password: first_paragraph_end: with a link to reset your password. Follow the link to continue resetting your password. diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml index 25242e0981f..2d2859cc323 100644 --- a/config/locales/notices/es.yml +++ b/config/locales/notices/es.yml @@ -2,8 +2,8 @@ es: notices: account_reactivation: "¡Estupendo! Tiene su clave personal." - dap_html: " " + dap_participation: Participamos en el programa analítico del Gobierno de Estados + Unidos. Vea los datos en analytics.usa.gov (en inglés). forgot_password: first_paragraph_end: con un enlace para restablecer su contraseña. Siga el enlace para continuar restableciendo su contraseña. diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml index 3db841b8198..3317236e881 100644 --- a/config/locales/notices/fr.yml +++ b/config/locales/notices/fr.yml @@ -2,8 +2,8 @@ fr: notices: account_reactivation: NOT TRANSLATED YET - dap_html: " " + dap_participation: Nous participons au programme d'analytique du gouvernement + des États-Unis. Consultez les données à analytics.usa.gov. forgot_password: first_paragraph_end: avec un lien pour réinitialiser votre mot de passe. Suivez le lien pour continuer à réinitialiser votre mot de passe. diff --git a/config/routes.rb b/config/routes.rb index 6565bcbd982..6797eb73f30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,6 +126,7 @@ get '/verify/activated' => 'verify#activated' get '/verify/address' => 'verify/address#index' get '/verify/cancel' => 'verify#cancel' + get '/verify/come_back_later' => 'verify/come_back_later#show' get '/verify/confirmations' => 'verify/confirmations#show' post '/verify/confirmations' => 'verify/confirmations#update' get '/verify/fail' => 'verify#fail' diff --git a/config/service_providers.yml b/config/service_providers.yml index 199a8dc5e22..8a839663cd6 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -397,3 +397,12 @@ production: logo: 'cbp-ttp.png' redirect_uris: - 'https://ttp.cbp.dhs.gov' + + # CBP OARS + 'urn:gov:dhs.cbp.pspd.oars:openidconnect:prod:app': + friendly_name: 'CBP OARS' + agency: 'DHS' + logo: 'cbp.png' + redirect_uris: + - 'gov.dhs.cbp.pspd.oars.user.prod://result' + allow_on_prod_chef_env: 'true' diff --git a/lib/config_validator.rb b/lib/config_validator.rb index 2242c44a19c..6166ae205b4 100644 --- a/lib/config_validator.rb +++ b/lib/config_validator.rb @@ -1,22 +1,37 @@ class ConfigValidator + ENV_PREFIX = Figaro::Application::FIGARO_ENV_PREFIX + def validate(env = ENV) bad_keys = keys_with_bad_values(env, candidate_keys(env)) return unless bad_keys.any? - raise warning(bad_keys).tr("\\\n", ' ') + raise warning(bad_keys).tr("\n", ' ') end private def candidate_keys(env) - env.keys.delete_if { |key| key.starts_with?(Figaro::Application::FIGARO_ENV_PREFIX) } + env.keys.keep_if { |key| candidate_key?(env, key) } + end + + def candidate_key?(env, key) + # A key is associated with a configuration setting if there are two + # settings in the environment: one with and without the Figaro prefix. + # We're only interested in the configuration settings and not other + # environment variables. + + env.include?(key) and env.include?(ENV_PREFIX + key) end def keys_with_bad_values(env, keys) - keys.keep_if { |key| %w[yes no].include?(env[key]) } + # Configuration settings for boolean values need to be "true/false" + # and not "yes/no". + + keys.keep_if { |key| %w[yes no].include?(env[key].strip.downcase) } end def warning(bad_keys) - "You have invalid values for #{bad_keys.uniq.to_sentence} in " \ - "config/application.yml. Please change them to true or false." + "You have invalid values (yes/no) for #{bad_keys.uniq.to_sentence} " \ + "in config/application.yml or your environment. " \ + "Please change them to true or false." end end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index e88d2e3f93e..881dfe453a2 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -1,5 +1,7 @@ class FeatureManagement - PT_DOMAIN_NAME = 'idp.pt.login.gov'.freeze + ENVS_WHERE_PREFILLING_OTP_ALLOWED = %w[ + idp.dev.login.gov idp.pt.login.gov + ].freeze ENVS_WHERE_PREFILLING_USPS_CODE_ALLOWED = %w[ idp.dev.login.gov idp.int.login.gov idp.qa.login.gov @@ -21,7 +23,7 @@ def self.development_and_telephony_disabled? end def self.prefill_otp_codes_allowed_in_production? - Figaro.env.domain_name == PT_DOMAIN_NAME && telephony_disabled? + ENVS_WHERE_PREFILLING_OTP_ALLOWED.include?(Figaro.env.domain_name) && telephony_disabled? end def self.enable_i18n_mode? diff --git a/scripts/load_testing/create_account.py b/scripts/load_testing/create_account.py index 33198ac4bc1..ad79e1f4aa8 100644 --- a/scripts/load_testing/create_account.py +++ b/scripts/load_testing/create_account.py @@ -52,8 +52,9 @@ def signup(self): dom = pyquery.PyQuery(resp.content) data = { '_method': 'patch', - 'two_factor_setup_form[phone]': '7035550001', - 'two_factor_setup_form[otp_delivery_preference]': 'sms', + 'user_phone_form[international_code]': 'US', + 'user_phone_form[phone]': '7035550001', + 'user_phone_form[otp_delivery_preference]': 'sms', 'authenticity_token': authenticity_token(dom), 'commit': 'Send security code', } diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 805e3f98393..46fcb4707a3 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -138,4 +138,41 @@ def index end end end + + describe '#session_expires_at' do + before { routes.draw { get 'index' => 'anonymous#index' } } + after { Rails.application.reload_routes! } + + controller do + prepend_before_action :session_expires_at + + def index + render text: 'Hello' + end + end + + context 'when URL contains the host parameter' do + it 'does not redirect to the host' do + get :index, timeout: true, host: 'www.monfresh.com' + + expect(response.header['Location']).to_not match 'www.monfresh.com' + end + end + + context 'when URL does not contain the timeout parameter' do + it 'does not redirect anywhere' do + get :index, host: 'www.monfresh.com' + + expect(response).to_not be_redirect + end + end + + context 'when URL contains the request_id parameter' do + it 'preserves the request_id parameter' do + get :index, timeout: true, request_id: '123' + + expect(response.header['Location']).to match '123' + end + end + end end 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 27cd54733b8..035e37cfe6c 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -389,8 +389,8 @@ expect(subject.current_user.reload.phone_confirmed_at).to eq @previous_phone_confirmed_at end - it 'redirects to verify_confirmations_path' do - expect(response).to redirect_to(verify_confirmations_path) + it 'redirects to verify_review_path' do + expect(response).to redirect_to(verify_review_path) end it 'does not call UserMailer' do diff --git a/spec/controllers/users/phones_controller_spec.rb b/spec/controllers/users/phones_controller_spec.rb index c4dedc1d413..1b3cebdb118 100644 --- a/spec/controllers/users/phones_controller_spec.rb +++ b/spec/controllers/users/phones_controller_spec.rb @@ -16,7 +16,9 @@ stub_analytics allow(@analytics).to receive(:track_event) - put :update, update_user_phone_form: { phone: new_phone, international_code: 'US' } + put :update, user_phone_form: { phone: new_phone, + international_code: 'US', + otp_delivery_preference: 'sms' } end it 'lets user know they need to confirm their new phone' do @@ -37,7 +39,9 @@ it 'does not delete the phone' do stub_sign_in(user) - put :update, update_user_phone_form: { phone: '', international_code: 'US' } + put :update, user_phone_form: { phone: '', + international_code: 'US', + otp_delivery_preference: 'sms' } expect(user.reload.phone).to be_present expect(response).to render_template(:edit) @@ -51,7 +55,9 @@ stub_analytics allow(@analytics).to receive(:track_event) - put :update, update_user_phone_form: { phone: second_user.phone, international_code: 'US' } + put :update, user_phone_form: { phone: second_user.phone, + international_code: 'US', + otp_delivery_preference: 'sms' } end it 'processes successfully and informs user' do @@ -74,7 +80,9 @@ user = build(:user, phone: '123-123-1234') stub_sign_in(user) - put :update, update_user_phone_form: { phone: invalid_phone, international_code: 'US' } + put :update, user_phone_form: { phone: invalid_phone, + international_code: 'US', + otp_delivery_preference: 'sms' } expect(user.phone).not_to eq invalid_phone expect(response).to render_template(:edit) @@ -85,7 +93,9 @@ it 'redirects to profile page without any messages' do stub_sign_in(user) - put :update, update_user_phone_form: { phone: user.phone, international_code: 'US' } + put :update, user_phone_form: { phone: user.phone, + international_code: 'US', + otp_delivery_preference: 'sms' } expect(response).to redirect_to account_url expect(flash.keys).to be_empty diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index 595764589f4..9f307f531a3 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -15,8 +15,7 @@ let(:user) { create(:user) } it 'tracks an event when the number is invalid' do - allow(subject).to receive(:authenticate_user).and_return(true) - allow(subject).to receive(:authorize_otp_setup).and_return(true) + sign_in(user) stub_analytics result = { @@ -28,7 +27,7 @@ expect(@analytics).to receive(:track_event). with(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result) - patch :set, two_factor_setup_form: { + patch :set, user_phone_form: { phone: '703-555-010', otp_delivery_preference: :sms, international_code: 'US', @@ -53,9 +52,9 @@ patch( :set, - two_factor_setup_form: { phone: '703-555-0100', - otp_delivery_preference: 'voice', - international_code: 'US' } + user_phone_form: { phone: '703-555-0100', + otp_delivery_preference: 'voice', + international_code: 'US' } ) expect(response).to redirect_to( @@ -85,9 +84,9 @@ patch( :set, - two_factor_setup_form: { phone: '703-555-0100', - otp_delivery_preference: :sms, - international_code: 'US' } + user_phone_form: { phone: '703-555-0100', + otp_delivery_preference: :sms, + international_code: 'US' } ) expect(response).to redirect_to( @@ -116,9 +115,9 @@ patch( :set, - two_factor_setup_form: { phone: '703-555-0100', - otp_delivery_preference: :sms, - international_code: 'US' } + user_phone_form: { phone: '703-555-0100', + otp_delivery_preference: :sms, + international_code: 'US' } ) expect(response).to redirect_to( diff --git a/spec/controllers/verify/come_back_later_controller_spec.rb b/spec/controllers/verify/come_back_later_controller_spec.rb new file mode 100644 index 00000000000..35cf9dbe442 --- /dev/null +++ b/spec/controllers/verify/come_back_later_controller_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +describe Verify::ComeBackLaterController do + let(:user) { build_stubbed(:user, :signed_up) } + let(:address_verification_mechanism) { 'usps' } + let(:profile) { build_stubbed(:profile, user: user) } + let(:idv_session) do + Idv::Session.new( + user_session: { context: :idv }, + current_user: user, + issuer: nil + ) + end + + before do + allow(idv_session).to receive(:address_verification_mechanism). + and_return(address_verification_mechanism) + allow(idv_session).to receive(:profile). + and_return(profile) + allow(subject).to receive(:idv_session).and_return(idv_session) + end + + context 'user has selected USPS address verification and has a complete profile' do + it 'renders the show template' do + get :show + + expect(response).to render_template :show + end + end + + context 'user has not selected USPS address verification' do + let(:address_verification_mechanism) { 'phone' } + + it 'redirects to the account path' do + get :show + + expect(response).to redirect_to account_path + end + end + + context 'does not have a complete profile' do + let(:profile) { nil } + + it 'redirects to the account path' do + get :show + + expect(response).to redirect_to account_path + end + end +end diff --git a/spec/controllers/verify/confirmations_controller_spec.rb b/spec/controllers/verify/confirmations_controller_spec.rb index 81bf90c13a3..824dbe8d680 100644 --- a/spec/controllers/verify/confirmations_controller_spec.rb +++ b/spec/controllers/verify/confirmations_controller_spec.rb @@ -140,11 +140,11 @@ def stub_idv_session expect(UspsConfirmation.count).to eq 1 end - it 'redirects to account page' do + it 'redirects to come back later page' do subject.session[:sp] = { loa3: true } patch :update - expect(response).to redirect_to account_url + expect(response).to redirect_to verify_come_back_later_path end end diff --git a/spec/controllers/verify/phone_controller_spec.rb b/spec/controllers/verify/phone_controller_spec.rb index c94a9742f3f..077ee3e5167 100644 --- a/spec/controllers/verify/phone_controller_spec.rb +++ b/spec/controllers/verify/phone_controller_spec.rb @@ -25,12 +25,34 @@ stub_verify_steps_one_and_two(user) end - it 'redirects to review when step is complete' do - subject.idv_session.vendor_phone_confirmation = true + context 'when the phone number is the same as the user phone' do + before do + subject.idv_session.params = { phone: user.phone } + end - get :new + it 'redirects to review when step is complete' do + subject.idv_session.vendor_phone_confirmation = true + get :new + + expect(response).to redirect_to verify_review_path + end + end + + context 'when the phone number is different from the user phone' do + before do + subject.idv_session.params = { phone: bad_phone } + end - expect(response).to redirect_to verify_review_path + it 'redirects to phone confirmation' do + subject.idv_session.vendor_phone_confirmation = true + get :new + + expect(response).to redirect_to redirect_to( + otp_send_path( + otp_delivery_selection_form: { otp_delivery_preference: 'sms' } + ) + ) + end end it 'redirects to fail when step attempts are exceeded' do @@ -61,7 +83,7 @@ it 'tracks form error and does not make a vendor API call' do expect(Idv::PhoneValidator).to_not receive(:new) - put :create, idv_phone_form: { phone: '703', international_code: 'US' } + put :create, idv_phone_form: { phone: '703' } result = { success: false, diff --git a/spec/controllers/verify/review_controller_spec.rb b/spec/controllers/verify/review_controller_spec.rb index 1c8a2e7731e..dbc1d1b21bd 100644 --- a/spec/controllers/verify/review_controller_spec.rb +++ b/spec/controllers/verify/review_controller_spec.rb @@ -100,6 +100,64 @@ def show end end + describe '#confirm_idv_phone_confirmed' do + controller do + before_action :confirm_idv_phone_confirmed + + def show + render text: 'Hello' + end + end + + before(:each) do + stub_sign_in(user) + allow(subject).to receive(:idv_session).and_return(idv_session) + routes.draw do + get 'show' => 'verify/review#show' + end + end + + context 'user is verifying by mail' do + before do + allow(idv_session).to receive(:address_verification_mechanism).and_return('usps') + end + + it 'does not redirect' do + get :show + + expect(response.body).to eq 'Hello' + end + end + + context 'user phone is confirmed' do + before do + allow(idv_session).to receive(:address_verification_mechanism).and_return('phone') + allow(idv_session).to receive(:phone_confirmed?).and_return(true) + end + + it 'does not redirect' do + get :show + + expect(response.body).to eq 'Hello' + end + end + + context 'user phone is not confirmed' do + before do + allow(idv_session).to receive(:address_verification_mechanism).and_return('phone') + allow(idv_session).to receive(:phone_confirmed?).and_return(false) + end + + it 'redirects to phone confirmation' do + get :show + + expect(response).to redirect_to otp_send_path( + otp_delivery_selection_form: { otp_delivery_preference: :sms } + ) + end + end + end + describe '#confirm_current_password' do controller do before_action :confirm_current_password @@ -271,27 +329,5 @@ def show expect(pii.first_name.norm).to eq 'JOSE' end end - - context 'user has entered different phone number from MFA' do - before do - idv_session.params = user_attrs.merge(phone: '213-555-1000') - idv_session.applicant = idv_session.vendor_params - idv_session.address_verification_mechanism = 'phone' - stub_analytics - allow(@analytics).to receive(:track_event) - end - - it 'redirects to phone confirmation path' do - put :create, user: { password: ControllerHelper::VALID_PASSWORD } - - expect(@analytics).to have_received(:track_event).with(Analytics::IDV_REVIEW_COMPLETE) - expect(response).to redirect_to( - otp_send_path( - otp_delivery_selection_form: { otp_delivery_preference: 'sms' } - ) - ) - expect(subject.user_session[:context]).to eq 'idv' - end - end end end diff --git a/spec/features/accessibility/idv_pages_spec.rb b/spec/features/accessibility/idv_pages_spec.rb index 290b9c68025..ef58a6f0f60 100644 --- a/spec/features/accessibility/idv_pages_spec.rb +++ b/spec/features/accessibility/idv_pages_spec.rb @@ -56,13 +56,13 @@ end scenario 'review page' do - sign_in_and_2fa_user + user = sign_in_and_2fa_user visit verify_session_path fill_out_idv_form_ok click_button t('forms.buttons.continue') fill_out_and_submit_finance_form click_idv_address_choose_phone - fill_out_phone_form_ok + fill_out_phone_form_ok(user.phone) click_button t('forms.buttons.continue') expect(current_path).to eq verify_review_path diff --git a/spec/features/flows/sp_authentication_flows_spec.rb b/spec/features/flows/sp_authentication_flows_spec.rb index b99eda80d5f..d18e09e429e 100644 --- a/spec/features/flows/sp_authentication_flows_spec.rb +++ b/spec/features/flows/sp_authentication_flows_spec.rb @@ -58,7 +58,7 @@ context 'with a valid phone number' do before do - fill_in 'two_factor_setup_form_phone', with: Faker::PhoneNumber.cell_phone + fill_in 'user_phone_form_phone', with: Faker::PhoneNumber.cell_phone end context 'with SMS delivery' do @@ -290,7 +290,7 @@ def submit! context 'with a valid phone number' do before do - fill_in 'two_factor_setup_form_phone', with: Faker::PhoneNumber.cell_phone + fill_in 'user_phone_form_phone', with: Faker::PhoneNumber.cell_phone end context 'with SMS delivery' do diff --git a/spec/features/idv/account_creation_spec.rb b/spec/features/idv/account_creation_spec.rb new file mode 100644 index 00000000000..c536072cfff --- /dev/null +++ b/spec/features/idv/account_creation_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +feature 'account creation after LOA3 request', idv_job: true do + include SamlAuthHelper + include IdvHelper + + context 'successful IdV with same phone as 2FA' do + it_behaves_like 'idv account creation', :saml + it_behaves_like 'idv account creation', :oidc + end + + context 'choosing USPS address verification' do + it_behaves_like 'selecting usps address verification method', :saml + it_behaves_like 'selecting usps address verification method', :oidc + end +end diff --git a/spec/features/idv/flow_spec.rb b/spec/features/idv/flow_spec.rb index 0b693cef18f..83cdb84597d 100644 --- a/spec/features/idv/flow_spec.rb +++ b/spec/features/idv/flow_spec.rb @@ -132,7 +132,7 @@ end scenario 'successful steps are not re-entrant, but are sticky on failure', js: true do - _user = sign_in_and_2fa_user + user = sign_in_and_2fa_user visit verify_session_path @@ -221,6 +221,7 @@ fill_out_phone_form_ok(good_phone_value) click_idv_continue + enter_correct_otp_code_for_user(user) page.find('.accordion').click @@ -383,11 +384,11 @@ end end - scenario 'continue phone OTP verification after cancel' do + scenario 'cancelling phone OTP verification redirects to verification cancel' do allow(Figaro.env).to receive(:otp_delivery_blocklist_maxretry).and_return('4') - different_phone = '555-555-9876' - user = sign_in_live_with_2fa + + sign_in_and_2fa_user visit verify_session_path fill_out_idv_form_ok @@ -397,24 +398,13 @@ click_idv_address_choose_phone fill_out_phone_form_ok(different_phone) click_idv_continue - fill_in :user_password, with: user_password - click_submit_default click_on t('links.cancel') - expect(current_path).to eq root_path - - sign_in_live_with_2fa(user) - - expect(page).to have_content('9876') - expect(page).to have_content(t('account.index.verification.instructions')) - - enter_correct_otp_code_for_user(user) - - expect(current_path).to eq account_path + expect(current_path).to eq verify_cancel_path end - scenario 'being unable to verify account without OTP phone confirmation' do + scenario 'attempting to skip OTP phone confirmation redirects to OTP confirmation', :js do different_phone = '555-555-9876' user = sign_in_live_with_2fa visit verify_session_path @@ -426,15 +416,13 @@ click_idv_address_choose_phone fill_out_phone_form_ok(different_phone) click_idv_continue - fill_in :user_password, with: user_password - click_submit_default - - visit verify_confirmations_path - click_acknowledge_personal_key + # Modify URL to skip phone confirmation + visit verify_review_path user.reload - expect(user.active_profile).to be_nil + expect(current_path).to eq(login_two_factor_path(otp_delivery_preference: :sms)) + expect(user.profiles).to be_empty end end diff --git a/spec/features/idv/phone_spec.rb b/spec/features/idv/phone_spec.rb index 2d824ef3117..b8423493066 100644 --- a/spec/features/idv/phone_spec.rb +++ b/spec/features/idv/phone_spec.rb @@ -31,14 +31,18 @@ phone: '+1 (416) 555-0190', password: Features::SessionHelper::VALID_PASSWORD ) + sign_in_and_2fa_user(user) visit verify_session_path - complete_idv_profile_with_phone('555-555-0000') + fill_in 'code', with: 'not a valid code 😟' + click_submit_default expect(page).to have_link t('forms.two_factor.try_again'), href: verify_phone_path enter_correct_otp_code_for_user(user) + fill_in :user_password, with: user_password + click_submit_default click_acknowledge_personal_key expect(current_path).to eq account_path @@ -79,7 +83,22 @@ fill_in 'Phone', with: '' find('#idv_phone_form_phone').native.send_keys('abcd1234') - expect(find('#idv_phone_form_phone').value).to eq '+1 234' + expect(find('#idv_phone_form_phone').value).to eq '1 (234) ' + end + + scenario 'phone field does not format international numbers', :js, idv_job: true do + sign_in_and_2fa_user + visit verify_session_path + fill_out_idv_form_ok + click_idv_continue + fill_out_financial_form_ok + click_idv_continue + + visit verify_phone_path + fill_in 'Phone', with: '' + find('#idv_phone_form_phone').native.send_keys('+81543543643') + + expect(find('#idv_phone_form_phone').value).to eq '+1 (815) 435-4364' end def complete_idv_profile_with_phone(phone) @@ -89,10 +108,6 @@ def complete_idv_profile_with_phone(phone) click_idv_continue click_idv_address_choose_phone fill_out_phone_form_ok(phone) - click_button t('forms.buttons.continue') - fill_in :user_password, with: user_password - click_submit_default - # choose default SMS delivery method for confirming this new number - click_submit_default + click_idv_continue end end diff --git a/spec/features/idv/usps_verification_spec.rb b/spec/features/idv/usps_verification_spec.rb new file mode 100644 index 00000000000..0815936b27f --- /dev/null +++ b/spec/features/idv/usps_verification_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +feature 'USPS verification' do + include SamlAuthHelper + include IdvHelper + + context 'signing in when profile is pending USPS verification' do + it_behaves_like 'signing in with pending USPS verification' + it_behaves_like 'signing in with pending USPS verification', :saml + it_behaves_like 'signing in with pending USPS verification', :oidc + end +end diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index ab2516c478b..b24b07de74a 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -271,26 +271,6 @@ ) end - context 'USPS verification' do - let(:phone_confirmed) { false } - - it 'prompts to finish verifying profile, then redirects to SP' do - allow(FeatureManagement).to receive(:reveal_usps_code?).and_return(true) - visit oidc_auth_url - - sign_in_live_with_2fa(user) - - click_button t('forms.verify_profile.submit') - - expect(current_path).to eq(sign_up_completed_path) - find('input').click - - redirect_uri = URI(current_url) - - expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') - end - end - context 'phone verification' do let(:phone_confirmed) { true } @@ -306,112 +286,6 @@ end end - context 'LOA3 signup' do - it 'redirects back to SP', email: true, idv_job: true do - allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) - - client_id = 'urn:gov:gsa:openidconnect:sp:server' - state = SecureRandom.hex - nonce = SecureRandom.hex - email = 'test@test.com' - - visit openid_connect_authorize_path( - client_id: client_id, - response_type: 'code', - acr_values: Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF, - scope: 'openid email profile:name social_security_number', - redirect_uri: 'http://localhost:7654/auth/result', - state: state, - prompt: 'select_account', - nonce: nonce - ) - - click_link t('sign_up.registrations.create_account') - submit_form_with_valid_email - click_confirmation_link_in_email(email) - submit_form_with_valid_password - set_up_2fa_with_valid_phone - enter_2fa_code - click_on 'Yes' - user = User.find_with_email(email) - complete_idv_profile_ok(user.reload) - click_acknowledge_personal_key - - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to_not have_content t('help_text.requested_attributes.address') - expect(page).to_not have_content t('help_text.requested_attributes.birthdate') - expect(page).to have_content t('help_text.requested_attributes.full_name') - expect(page).to_not have_content t('help_text.requested_attributes.phone') - expect(page).to have_content t('help_text.requested_attributes.social_security_number') - end - - expect(page.response_headers['Content-Security-Policy']). - to(include('form-action \'self\' http://localhost:7654')) - - click_on I18n.t('forms.buttons.continue') - - redirect_uri = URI(current_url) - redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access - - expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') - expect(redirect_params[:state]).to eq(state) - - code = redirect_params[:code] - expect(code).to be_present - - jwt_payload = { - iss: client_id, - sub: client_id, - aud: api_openid_connect_token_url, - jti: SecureRandom.hex, - exp: 5.minutes.from_now.to_i, - } - - client_assertion = JWT.encode(jwt_payload, client_private_key, 'RS256') - client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' - - page.driver.post api_openid_connect_token_path, - grant_type: 'authorization_code', - code: code, - client_assertion_type: client_assertion_type, - client_assertion: client_assertion - - expect(page.status_code).to eq(200) - token_response = JSON.parse(page.body).with_indifferent_access - - id_token = token_response[:id_token] - expect(id_token).to be_present - - decoded_id_token, _headers = JWT.decode( - id_token, sp_public_key, true, algorithm: 'RS256' - ).map(&:with_indifferent_access) - - sub = decoded_id_token[:sub] - expect(sub).to be_present - expect(decoded_id_token[:nonce]).to eq(nonce) - expect(decoded_id_token[:aud]).to eq(client_id) - expect(decoded_id_token[:acr]).to eq(Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF) - expect(decoded_id_token[:iss]).to eq(root_url) - expect(decoded_id_token[:email]).to eq(user.email) - expect(decoded_id_token[:given_name]).to eq('José') - expect(decoded_id_token[:social_security_number]).to eq('666-66-1234') - - access_token = token_response[:access_token] - expect(access_token).to be_present - - page.driver.get api_openid_connect_userinfo_path, - {}, - 'HTTP_AUTHORIZATION' => "Bearer #{access_token}" - - userinfo_response = JSON.parse(page.body).with_indifferent_access - expect(userinfo_response[:sub]).to eq(sub) - expect(userinfo_response[:email]).to eq(user.email) - expect(userinfo_response[:given_name]).to eq('José') - expect(userinfo_response[:social_security_number]).to eq('666-66-1234') - end - end - context 'visiting IdP via SP, then going back to SP and visiting IdP again' do it 'displays the branded page' do visit_idp_from_sp_with_loa1 diff --git a/spec/features/saml/loa3_sso_spec.rb b/spec/features/saml/loa3_sso_spec.rb index f2014fb7225..c713daec8a8 100644 --- a/spec/features/saml/loa3_sso_spec.rb +++ b/spec/features/saml/loa3_sso_spec.rb @@ -4,10 +4,13 @@ include SamlAuthHelper include IdvHelper - def perform_id_verification_with_usps_without_confirming_code_then_sign_out(user) + def perform_id_verification_with_usps_without_confirming_code(user) + allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) visit saml_authn_request - sign_in_live_with_2fa(user) + click_link t('links.sign_in') + fill_in_credentials_and_submit(user.email, user.password) + click_submit_default click_idv_begin fill_out_idv_form_ok click_idv_continue @@ -18,6 +21,10 @@ def perform_id_verification_with_usps_without_confirming_code_then_sign_out(user fill_in :user_password, with: user.password click_submit_default click_acknowledge_personal_key + click_link t('idv.buttons.return_to_account') + end + + def sign_out_user first(:link, t('links.sign_out')).click click_submit_default end @@ -29,81 +36,6 @@ def perform_id_verification_with_usps_without_confirming_code_then_sign_out(user @saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) end - it 'redirects to original SAML Authn Request after IdV is complete', email: true do - xmldoc = SamlResponseDoc.new('feature', 'response_assertion') - - visit @saml_authn_request - - saml_register_loa3_user(email) - - expect(current_path).to eq verify_path - - click_idv_begin - - user = User.find_with_email(email) - complete_idv_profile_ok(user.reload) - click_acknowledge_personal_key - - expect(page).to have_content t( - 'titles.sign_up.completion_html', - accent: t('titles.sign_up.loa3'), - app: APP_NAME - ) - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to_not have_content t('help_text.requested_attributes.address') - expect(page).to_not have_content t('help_text.requested_attributes.birthdate') - expect(page).to have_content t('help_text.requested_attributes.full_name') - expect(page).to have_content t('help_text.requested_attributes.phone') - expect(page).to have_content t('help_text.requested_attributes.social_security_number') - end - - click_on I18n.t('forms.buttons.continue') - expect(current_url).to eq @saml_authn_request - - user_access_key = user.unlock_user_access_key(Features::SessionHelper::VALID_PASSWORD) - profile_phone = user.active_profile.decrypt_pii(user_access_key).phone - - expect(user.events.account_verified.size).to be(1) - expect(xmldoc.phone_number.children.children.to_s).to eq(profile_phone) - end - - it 'allows the user to select verification via USPS letter', email: true do - visit @saml_authn_request - - saml_register_loa3_user(email) - - click_idv_begin - - fill_out_idv_form_ok - click_idv_continue - fill_out_financial_form_ok - click_idv_continue - - click_idv_address_choose_usps - - click_on t('idv.buttons.mail.send') - - expect(current_path).to eq verify_review_path - expect(page).to_not have_content t('idv.messages.phone.phone_of_record') - - fill_in :user_password, with: user_password - - expect { click_submit_default }. - to change { UspsConfirmation.count }.from(0).to(1) - - expect(current_url).to eq verify_confirmations_url - click_acknowledge_personal_key - - expect(User.find_with_email(email).events.account_verified.size).to be(0) - expect(current_url).to eq(account_url) - expect(page).to have_content(t('account.index.verification.reactivate_button')) - - usps_confirmation_entry = UspsConfirmation.last.decrypted_entry - expect(usps_confirmation_entry.issuer). - to eq('https://rp1.serviceprovider.com/auth/saml/metadata') - end - it 'shows user the start page with accordion' do saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) sp_content = [ @@ -197,39 +129,47 @@ def perform_id_verification_with_usps_without_confirming_code_then_sign_out(user context 'having previously selected USPS verification' do let(:phone_confirmed) { false } - it 'prompts for confirmation code at sign in' do - allow(FeatureManagement).to receive(:reveal_usps_code?).and_return(true) + context 'provides an option to send another letter' do + it 'without signing out' do + user = create(:user, :signed_up) - saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) - visit saml_authn_request - sign_in_live_with_2fa(user) + perform_id_verification_with_usps_without_confirming_code(user) - expect(current_path).to eq verify_account_path - expect(page).to have_content t('idv.messages.usps.resend') + expect(current_path).to eq account_path - click_button t('forms.verify_profile.submit') + click_link(t('account.index.verification.reactivate_button')) - expect(user.events.account_verified.size).to be(1) - expect(current_path).to eq(sign_up_completed_path) + expect(current_path).to eq verify_account_path - find('input').click + click_link(t('idv.messages.usps.resend')) - expect(current_url).to eq saml_authn_request - end + expect(user.events.account_verified.size).to be(0) + expect(current_path).to eq(verify_usps_path) - it 'provides an option to send another letter' do - user = create(:user, :signed_up) + click_button(t('idv.buttons.mail.resend')) - perform_id_verification_with_usps_without_confirming_code_then_sign_out(user) + expect(current_path).to eq(account_path) + end - sign_in_live_with_2fa(user) + it 'after signing out' do + user = create(:user, :signed_up) + + perform_id_verification_with_usps_without_confirming_code(user) + sign_out_user + + sign_in_live_with_2fa(user) + + expect(current_path).to eq verify_account_path + + click_link(t('idv.messages.usps.resend')) - expect(current_path).to eq verify_account_path + expect(user.events.account_verified.size).to be(0) + expect(current_path).to eq(verify_usps_path) - click_link(t('idv.messages.usps.resend')) + click_button(t('idv.buttons.mail.resend')) - expect(user.events.account_verified.size).to be(0) - expect(current_path).to eq(verify_usps_path) + expect(current_path).to eq(account_path) + 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 bfa396bcafb..389ab0641aa 100644 --- a/spec/features/two_factor_authentication/change_factor_spec.rb +++ b/spec/features/two_factor_authentication/change_factor_spec.rb @@ -178,7 +178,7 @@ def complete_2fa_confirmation_without_entering_otp end def update_phone_number(phone = '703-555-0100') - fill_in 'update_user_phone_form[phone]', with: phone + fill_in 'user_phone_form[phone]', with: phone click_button t('forms.buttons.submit.confirm_change') end diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index 7ce7aff8dd1..5a84a580bdd 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -54,7 +54,7 @@ fill_in 'Phone', with: guam_phone phone_radio_button = page.find( - '#two_factor_setup_form_otp_delivery_preference_voice', + '#user_phone_form_otp_delivery_preference_voice', visible: :all ) @@ -94,7 +94,7 @@ select 'Turkey +90', from: 'International code' fill_in 'Phone', with: '+90 312 213 29 65' phone_radio_button = page.find( - '#two_factor_setup_form_otp_delivery_preference_voice', + '#user_phone_form_otp_delivery_preference_voice', visible: :all ) @@ -117,17 +117,17 @@ sign_in_before_2fa fill_in 'Phone', with: '+81 54 354 3643' - expect(page.find('#two_factor_setup_form_international_code').value).to eq 'JP' + expect(page.find('#user_phone_form_international_code').value).to eq 'JP' fill_in 'Phone', with: '5376' select 'Morocco +212', from: 'International code' - expect(find('#two_factor_setup_form_phone').value).to eq '+212 5376' + expect(find('#user_phone_form_phone').value).to eq '+212 5376' fill_in 'Phone', with: '54354' select 'Japan +81', from: 'International code' - expect(find('#two_factor_setup_form_phone').value).to include '+81' + expect(find('#user_phone_form_phone').value).to include '+81' end end end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 7ba57d8ecb5..babbe5f573b 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -118,14 +118,14 @@ it 'refreshes the current page after session expires', js: true do allow(Devise).to receive(:timeout_in).and_return(1) - visit sign_up_email_path(foo: 'bar') + visit sign_up_email_path(request_id: '123abc') fill_in 'Email', with: 'test@example.com' expect(page).to have_content( t('notices.session_cleared', minutes: Figaro.env.session_timeout_in_minutes) ) expect(page).to have_field('Email', with: '') - expect(current_url).to match Regexp.escape(sign_up_email_path(foo: 'bar')) + expect(current_url).to match Regexp.escape(sign_up_email_path(request_id: '123abc')) end it 'does not refresh the page after the session expires', js: true do diff --git a/spec/features/users/user_edit_spec.rb b/spec/features/users/user_edit_spec.rb index 90810805be6..7a2ae72b0d0 100644 --- a/spec/features/users/user_edit_spec.rb +++ b/spec/features/users/user_edit_spec.rb @@ -1,9 +1,11 @@ require 'rails_helper' feature 'User edit' do + let(:user) { create(:user, :signed_up) } + context 'editing email' do before do - sign_in_and_2fa_user + sign_in_and_2fa_user(user) visit manage_email_path end @@ -17,7 +19,7 @@ context 'editing 2FA phone number' do before do - sign_in_and_2fa_user + sign_in_and_2fa_user(user) visit manage_phone_path end @@ -31,17 +33,34 @@ scenario 'updates international code as user types', :js do fill_in 'Phone', with: '+81 54 354 3643' - expect(page.find('#update_user_phone_form_international_code').value).to eq 'JP' + expect(page.find('#user_phone_form_international_code').value).to eq 'JP' fill_in 'Phone', with: '5376' select 'Morocco +212', from: 'International code' - expect(find('#update_user_phone_form_phone').value).to eq '+212 5376' + expect(find('#user_phone_form_phone').value).to eq '+212 5376' fill_in 'Phone', with: '54354' select 'Japan +81', from: 'International code' - expect(find('#update_user_phone_form_phone').value).to include '+81' + expect(find('#user_phone_form_phone').value).to include '+81' + end + + scenario 'confirms with selected OTP delivery method and updates user delivery preference' do + allow(SmsOtpSenderJob).to receive(:perform_later) + allow(VoiceOtpSenderJob).to receive(:perform_later) + + fill_in 'Phone', with: '555-555-5000' + choose 'Phone call' + + click_button t('forms.buttons.submit.confirm_change') + + user.reload + + expect(current_path).to eq(login_otp_path(otp_delivery_preference: :voice)) + expect(SmsOtpSenderJob).to_not have_received(:perform_later) + expect(VoiceOtpSenderJob).to have_received(:perform_later) + expect(user.otp_delivery_preference).to eq('voice') end end end diff --git a/spec/features/users/verify_profile_spec.rb b/spec/features/users/verify_profile_spec.rb index ed8942ab3b8..9babf534b43 100644 --- a/spec/features/users/verify_profile_spec.rb +++ b/spec/features/users/verify_profile_spec.rb @@ -17,18 +17,6 @@ context 'USPS letter' do let(:phone_confirmed) { false } - scenario 'received OTP via USPS' do - sign_in_live_with_2fa(user) - - expect(current_path).to eq verify_account_path - - fill_in t('forms.verify_profile.name'), with: otp - click_button t('forms.verify_profile.submit') - - expect(current_path).to eq account_path - expect(page).to_not have_content(t('account.index.verification.reactivate_button')) - end - xscenario 'OTP has expired' do # see https://github.com/18F/identity-private/issues/1108#issuecomment-293328267 end diff --git a/spec/features/visitors/email_confirmation_spec.rb b/spec/features/visitors/email_confirmation_spec.rb index f57613a64d7..53505387407 100644 --- a/spec/features/visitors/email_confirmation_spec.rb +++ b/spec/features/visitors/email_confirmation_spec.rb @@ -9,7 +9,7 @@ open_email(email) visit_in_email(t('mailer.confirmation_instructions.link_text')) - expect(page.html).not_to include(t('notices.dap_html')) + expect(page.html).not_to include(t('notices.dap_participation')) expect(page).to have_content t('devise.confirmations.confirmed_but_must_set_password') expect(page).to have_title t('titles.confirmations.show') expect(page).to have_content t('forms.confirmation.show_hdr') @@ -73,7 +73,7 @@ visit destroy_user_session_url visit sign_up_create_email_confirmation_url(confirmation_token: @raw_confirmation_token) - expect(page.html).to include(t('notices.dap_html')) + expect(page.html).to include(t('notices.dap_participation')) expect(page).to have_content( t('devise.confirmations.already_confirmed', action: 'Please sign in.') ) diff --git a/spec/features/visitors/password_recovery_spec.rb b/spec/features/visitors/password_recovery_spec.rb index c4f5a0b37c6..a32d60397cc 100644 --- a/spec/features/visitors/password_recovery_spec.rb +++ b/spec/features/visitors/password_recovery_spec.rb @@ -28,7 +28,7 @@ open_last_email click_email_link_matching(/reset_password_token/) - expect(page.html).not_to include(t('notices.dap_html')) + expect(page.html).not_to include(t('notices.dap_participation')) expect(current_path).to eq edit_user_password_path end end diff --git a/spec/forms/idv/phone_form_spec.rb b/spec/forms/idv/phone_form_spec.rb index 0dcf1a3bf25..001096d9b56 100644 --- a/spec/forms/idv/phone_form_spec.rb +++ b/spec/forms/idv/phone_form_spec.rb @@ -2,6 +2,8 @@ describe Idv::PhoneForm do let(:user) { build_stubbed(:user, :signed_up) } + let(:params) { { phone: '555-555-5000' } } + subject { Idv::PhoneForm.new({}, user) } it_behaves_like 'a phone form' @@ -9,7 +11,7 @@ describe '#submit' do context 'when the form is valid' do it 'returns a successful form response' do - result = subject.submit(phone: '703-555-1212', international_code: 'US') + result = subject.submit(phone: '703-555-1212') expect(result).to be_kind_of(FormResponse) expect(result.success?).to eq(true) @@ -17,7 +19,7 @@ end it 'adds phone key to idv_params' do - subject.submit(phone: '703-555-1212', international_code: 'US') + subject.submit(phone: '703-555-1212') expected_params = { phone: '7035551212', @@ -29,7 +31,7 @@ context 'when the form is invalid' do it 'returns an unsuccessful form response' do - result = subject.submit(phone: 'Im not a phone number 🙃', international_code: 'US') + result = subject.submit(phone: 'Im not a phone number 🙃') expect(result).to be_kind_of(FormResponse) expect(result.success?).to eq(false) @@ -38,7 +40,7 @@ end it 'adds phone_confirmed_at key to idv_params when submitted phone equals user phone' do - subject.submit(phone: '+1 (202) 555-1212', international_code: 'US') + subject.submit(phone: '+1 (202) 555-1212') expected_params = { phone: '2025551212', @@ -47,5 +49,26 @@ expect(subject.idv_params).to eq expected_params end + + it 'uses the user phone number as the initial phone value' do + user = build_stubbed(:user, :signed_up, phone: '555-555-1234') + subject = Idv::PhoneForm.new({}, user) + + expect(subject.phone).to eq('+1 (555) 555-1234') + end + + it 'does not use an international number as the initial phone value' do + user = build_stubbed(:user, :signed_up, phone: '+81 54 354 3643') + subject = Idv::PhoneForm.new({}, user) + + 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') + + expect(result.success?).to eq(false) + expect(result.errors[:phone]).to include(t('errors.messages.must_have_us_country_code')) + end end end diff --git a/spec/forms/two_factor_setup_form_spec.rb b/spec/forms/two_factor_setup_form_spec.rb deleted file mode 100644 index 7af36cef7a6..00000000000 --- a/spec/forms/two_factor_setup_form_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'rails_helper' - -describe TwoFactorSetupForm, type: :model do - let(:user) { build_stubbed(:user) } - let(:phone) { '+1 (202) 202-2020' } - let(:international_code) { 'US' } - let(:params) do - { - phone: phone, - international_code: 'US', - otp_delivery_preference: 'sms', - } - end - subject { TwoFactorSetupForm.new(user) } - - it_behaves_like 'an otp delivery preference form' - - it do - is_expected. - to validate_presence_of(:phone). - with_message(t('errors.messages.improbable_phone')) - 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 - user = build_stubbed(:user, :signed_up, phone: '+1 (202) 555-1213') - allow(User).to receive(:exists?).with(phone: user.phone).and_return(true) - form = TwoFactorSetupForm.new(user) - extra = { - otp_delivery_preference: 'sms', - } - result = instance_double(FormResponse) - - params[:phone] = user.phone - - expect(FormResponse).to receive(:new). - with(success: true, errors: {}, extra: extra).and_return(result) - expect(form.submit(params)).to eq result - end - end - - context 'when phone is not already taken' do - let(:phone) { '+1 (703) 555-1212' } - - it 'is valid' do - extra = { - otp_delivery_preference: 'sms', - } - result = instance_double(FormResponse) - - expect(FormResponse).to receive(:new). - with(success: true, errors: {}, extra: extra).and_return(result) - expect(subject.submit(params)).to eq result - end - end - - context 'when phone is same as current user' do - it 'is valid' do - user = build_stubbed(:user, phone: phone) - form = TwoFactorSetupForm.new(user) - extra = { - otp_delivery_preference: 'sms', - } - result = instance_double(FormResponse) - - expect(FormResponse).to receive(:new). - with(success: true, errors: {}, extra: extra).and_return(result) - expect(form.submit(params)).to eq result - end - end - - context 'when phone is empty' do - let(:phone) { '' } - - it 'does not add already taken errors' do - errors = { - phone: [t('errors.messages.improbable_phone')], - } - extra = { - otp_delivery_preference: 'sms', - } - result = instance_double(FormResponse) - - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(subject.submit(params)).to eq result - end - end - end - - describe '#submit' do - context 'when otp_delivery_preference is the same as the user otp_delivery_preference' do - it 'does not update the user' do - user = build_stubbed(:user, otp_delivery_preference: 'sms') - form = TwoFactorSetupForm.new(user) - - expect(UpdateUser).to_not receive(:new) - - form.submit(params) - end - end - - context 'when otp_delivery_preference is different from the user otp_delivery_preference' do - it 'updates the user' do - user = build_stubbed(:user, otp_delivery_preference: 'voice') - form = TwoFactorSetupForm.new(user) - attributes = { otp_delivery_preference: 'sms' } - - updated_user = instance_double(UpdateUser) - allow(UpdateUser).to receive(:new). - with(user: user, attributes: attributes).and_return(updated_user) - - expect(updated_user).to receive(:call) - - form.submit(params) - end - end - end -end diff --git a/spec/forms/update_user_phone_form_spec.rb b/spec/forms/update_user_phone_form_spec.rb deleted file mode 100644 index 45a513005c8..00000000000 --- a/spec/forms/update_user_phone_form_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'rails_helper' - -describe UpdateUserPhoneForm do - let(:user) { build_stubbed(:user, :signed_up) } - subject { UpdateUserPhoneForm.new(user) } - - it_behaves_like 'a phone form' -end diff --git a/spec/forms/user_phone_form_spec.rb b/spec/forms/user_phone_form_spec.rb new file mode 100644 index 00000000000..5e6cc1b32d9 --- /dev/null +++ b/spec/forms/user_phone_form_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +describe UserPhoneForm do + let(:user) { build(:user, :signed_up) } + let(:params) do + { + phone: '555-555-5000', + international_code: 'US', + otp_delivery_preference: 'sms', + } + end + subject { UserPhoneForm.new(user) } + + it_behaves_like 'a phone form' + + it 'loads initial values from the user object' do + user = build_stubbed( + :user, + phone: '+1 (555) 500-5000', + otp_delivery_preference: 'voice' + ) + subject = UserPhoneForm.new(user) + + expect(subject.phone).to eq(user.phone) + expect(subject.international_code).to eq('US') + expect(subject.otp_delivery_preference).to eq(user.otp_delivery_preference) + end + + it 'infers the international code from the user phone number' do + user = build_stubbed(:user, phone: '+81 744 21 1234') + subject = UserPhoneForm.new(user) + + expect(subject.international_code).to eq('JP') + end + + describe '#submit' do + context 'when phone is valid' do + it 'is valid' do + result = subject.submit(params) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + + it 'include otp preference in the form response extra' do + result = subject.submit(params) + + expect(result.extra).to eq( + otp_delivery_preference: params[:otp_delivery_preference] + ) + end + + it 'does not update the user phone attribute' do + user = create(:user) + subject = UserPhoneForm.new(user) + params[:phone] = '+1 504 444 1643' + + subject.submit(params) + + user.reload + expect(user.phone).to_not eq('+1 504 444 1643') + end + + it 'preserves the format of the submitted phone number if phone is invalid' do + params[:phone] = '555-555-5000' + params[:international_code] = 'MA' + + result = subject.submit(params) + + expect(result.success?).to eq(false) + expect(subject.phone).to eq('555-555-5000') + end + end + + context 'when otp_delivery_preference is voice and phone number does not support voice' do + let(:guam_phone) { '671-555-5000' } + let(:params) do + { + phone: guam_phone, + international_code: 'US', + otp_delivery_preference: 'voice', + } + end + + it 'is invalid' do + result = subject.submit(params) + + expect(result.success?).to eq(false) + end + end + end + + describe '#phone_changed?' do + it 'returns true if the user phone has changed' do + params[:phone] = '+1 504 444 1643' + subject.submit(params) + + expect(subject.phone_changed?).to eq(true) + end + + it 'returns false if the user phone has not changed' do + params[:phone] = user.phone + subject.submit(params) + + expect(subject.phone_changed?).to eq(false) + end + end +end diff --git a/spec/lib/config_validator_spec.rb b/spec/lib/config_validator_spec.rb index 97bb4f1cdfb..cb0a73ee990 100644 --- a/spec/lib/config_validator_spec.rb +++ b/spec/lib/config_validator_spec.rb @@ -5,19 +5,34 @@ it 'raises if one or more candidate key values is set to yes or no' do bad_key = 'bad_key' other_bad_key = 'other_bad_key' - noncandidate_key = '_FIGARO_KEY' + up_bad_key = 'up_bad_key' + cap_bad_key = 'cap_bad_key' + noncandidate_key = 'KEY' good_key = 'good_key' env = { bad_key => 'yes', other_bad_key => 'no', + up_bad_key => 'YES ', + cap_bad_key => ' No', noncandidate_key => 'yes', good_key => 'foo', } + # Figaro sets 2 environment variables for each configuration: + # 1 with and 1 without the Figaro prefix. Settings that don't + # have both are not part of the configuration. This mimics + # Figaro by adding the settings with the prefix. + + [bad_key, other_bad_key, up_bad_key, cap_bad_key, good_key].each do |key| + env[ConfigValidator::ENV_PREFIX + key] = env[key] + end + + list = "#{bad_key}, #{other_bad_key}, #{up_bad_key}, and #{cap_bad_key}" + expect { ConfigValidator.new.validate(env) }.to raise_error( RuntimeError, - /You have invalid values for #{bad_key} and #{other_bad_key}/ + %r{You have invalid values \(yes\/no\) for #{list}} ) end end diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb index 676fe6ef723..dd1adf85677 100644 --- a/spec/lib/feature_management_spec.rb +++ b/spec/lib/feature_management_spec.rb @@ -18,14 +18,35 @@ end end - context 'when the server is idp.pt.login.gov' do - before { allow(FeatureManagement).to receive(:telephony_disabled?).and_return(true) } - - it 'returns true in production mode' do + context 'in production servers' do + before do + allow(FeatureManagement).to receive(:telephony_disabled?).and_return(true) allow(Rails.env).to receive(:production?).and_return(true) - allow(Figaro.env).to receive(:domain_name).and_return(FeatureManagement::PT_DOMAIN_NAME) + allow(Figaro.env).to receive(:domain_name).and_return(domain_name) + end - expect(FeatureManagement.prefill_otp_codes?).to eq(true) + context 'when the server is idp.pt.login.gov' do + let(:domain_name) { 'idp.pt.login.gov' } + + it 'prefills codes' do + expect(FeatureManagement.prefill_otp_codes?).to eq(true) + end + end + + context 'when the server is idp.dev.login.gov' do + let(:domain_name) { 'idp.dev.login.gov' } + + it 'prefills codes' do + expect(FeatureManagement.prefill_otp_codes?).to eq(true) + end + end + + context 'when the server is idp.staging.login.gov' do + let(:domain_name) { 'idp.staging.login.gov' } + + it 'does not prefill codes' do + expect(FeatureManagement.prefill_otp_codes?).to eq(false) + end end end @@ -46,7 +67,7 @@ it 'returns false in production mode when server is pt' do allow(Rails.env).to receive(:production?).and_return(true) - allow(Figaro.env).to receive(:domain_name).and_return(FeatureManagement::PT_DOMAIN_NAME) + allow(Figaro.env).to receive(:domain_name).and_return('idp.pt.login.gov') expect(FeatureManagement.prefill_otp_codes?).to eq(false) end diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb index 2a9aa55a07e..87174d43806 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 @@ -1,17 +1,29 @@ require 'rails_helper' describe TwoFactorAuthCode::PhoneDeliveryPresenter do + include Rails.application.routes.url_helpers + + let(:view) { ActionController::Base.new.view_context } let(:data) do { - code_value: '123abc', - totp_enabled: false, - phone_number: '***-***-5000', - unconfirmed_phone: false, + confirmation_for_phone_change: false, + confirmation_for_idv: false, + phone_number: '5555559876', + code_value: '999999', otp_delivery_preference: 'sms', + reenter_phone_number_path: '/verify/phone', + unconfirmed_phone: true, + totp_enabled: false, + personal_key_unavailable: true, + reauthn: false, } end - let(:view) { ActionController::Base.new.view_context } - let(:presenter) { TwoFactorAuthCode::PhoneDeliveryPresenter.new(data: data, view: view) } + let(:presenter) do + TwoFactorAuthCode::PhoneDeliveryPresenter.new( + data: data, + view: view + ) + end it 'is a subclass of GenericDeliveryPresenter' do expect(TwoFactorAuthCode::PhoneDeliveryPresenter.superclass).to( @@ -19,6 +31,27 @@ ) end + describe '#cancel_link' do + it 'returns the sign out path during authentication' do + expect(presenter.cancel_link).to eq sign_out_path + end + + it 'returns the account path during reauthn' do + data[:reauthn] = true + expect(presenter.cancel_link).to eq account_path + end + + it 'returns the account path during phone change confirmation' do + data[:confirmation_for_phone_change] = true + expect(presenter.cancel_link).to eq account_path + end + + it 'returns the verification cancel path during identity verification' do + data[:confirmation_for_idv] = true + expect(presenter.cancel_link).to eq verify_cancel_path + end + end + describe '#fallback_links' do context 'with totp enabled' do before do diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index 3e95c0cbbd5..66c2cb8ae71 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -56,6 +56,19 @@ expect(subject).not_to have_received(:complete_profile) end end + + context 'without a confirmed phone number' do + before do + subject.address_verification_mechanism = :phone + subject.vendor_phone_confirmation = false + end + + it 'does not complete the user profile' do + allow(subject).to receive(:complete_profile) + subject.complete_session + expect(subject).not_to have_received(:complete_profile) + end + end end describe '#phone_confirmed?' do diff --git a/spec/services/request_key_manager_spec.rb b/spec/services/request_key_manager_spec.rb index 7654a5f5c56..f1876202400 100644 --- a/spec/services/request_key_manager_spec.rb +++ b/spec/services/request_key_manager_spec.rb @@ -1,11 +1,19 @@ require 'rails_helper' describe RequestKeyManager do - describe '#equifax_ssh_key' do + describe '.equifax_ssh_key' do it 'initializes' do ssh_key = described_class.equifax_ssh_key expect(ssh_key).to be_a OpenSSL::PKey::RSA end end + + describe '.private_key' do + it 'initializes' do + ssh_key = described_class.private_key + + expect(ssh_key).to be_a OpenSSL::PKey::RSA + end + end end diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb index a228756ac77..ebb40023a86 100644 --- a/spec/support/features/idv_helper.rb +++ b/spec/support/features/idv_helper.rb @@ -96,4 +96,29 @@ def complete_idv_profile_ok(user, password = user_password) fill_in 'Password', with: password click_submit_default end + + def visit_idp_from_sp_with_loa3(sp) + if sp == :saml + @saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) + visit @saml_authn_request + elsif sp == :oidc + @state = SecureRandom.hex + @client_id = 'urn:gov:gsa:openidconnect:sp:server' + @nonce = SecureRandom.hex + visit_idp_from_oidc_sp_with_loa3(state: @state, client_id: @client_id, nonce: @nonce) + end + end + + def visit_idp_from_oidc_sp_with_loa3(state: SecureRandom.hex, client_id:, nonce:) + visit openid_connect_authorize_path( + client_id: client_id, + response_type: 'code', + acr_values: Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF, + scope: 'openid email profile:name phone social_security_number', + redirect_uri: 'http://localhost:7654/auth/result', + state: state, + prompt: 'select_account', + nonce: nonce + ) + end end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 63da64d2b21..7d6aa882d6b 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -350,5 +350,23 @@ def set_up_2fa_with_valid_phone fill_in 'Phone', with: '202-555-1212' click_send_security_code end + + def register_user(email = 'test@test.com') + allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) + click_link t('sign_up.registrations.create_account') + submit_form_with_valid_email(email) + click_confirmation_link_in_email(email) + submit_form_with_valid_password + set_up_2fa_with_valid_phone + enter_2fa_code + User.find_with_email(email) + end + + def sign_in_via_branded_page(user) + allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) + click_link t('links.sign_in') + fill_in_credentials_and_submit(user.email, user.password) + click_submit_default + end end end diff --git a/spec/support/idv_examples/account_creation.rb b/spec/support/idv_examples/account_creation.rb new file mode 100644 index 00000000000..a326ef74717 --- /dev/null +++ b/spec/support/idv_examples/account_creation.rb @@ -0,0 +1,127 @@ +shared_examples 'idv account creation' do |sp| + it 'redirects to SP after IdV is complete', email: true do + email = 'test@test.com' + + visit_idp_from_sp_with_loa3(sp) + + register_user(email) + + expect(current_path).to eq verify_path + + click_idv_begin + + user = User.find_with_email(email) + complete_idv_profile_ok(user.reload) + click_acknowledge_personal_key + + expect(page).to have_content t( + 'titles.sign_up.completion_html', + accent: t('titles.sign_up.loa3'), + app: APP_NAME + ) + within('.requested-attributes') do + expect(page).to have_content t('help_text.requested_attributes.email') + expect(page).to_not have_content t('help_text.requested_attributes.address') + expect(page).to_not have_content t('help_text.requested_attributes.birthdate') + expect(page).to have_content t('help_text.requested_attributes.full_name') + expect(page).to have_content t('help_text.requested_attributes.phone') + expect(page).to have_content t('help_text.requested_attributes.social_security_number') + end + + if sp == :oidc + expect(page.response_headers['Content-Security-Policy']). + to(include('form-action \'self\' http://localhost:7654')) + end + + click_on I18n.t('forms.buttons.continue') + + expect(user.events.account_verified.size).to be(1) + + if sp == :saml + user_access_key = user.unlock_user_access_key(Features::SessionHelper::VALID_PASSWORD) + profile_phone = user.active_profile.decrypt_pii(user_access_key).phone + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + + expect(current_url).to eq @saml_authn_request + expect(xmldoc.phone_number.children.children.to_s).to eq(profile_phone) + end + + if sp == :oidc + redirect_uri = URI(current_url) + redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access + + expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') + expect(redirect_params[:state]).to eq(@state) + + code = redirect_params[:code] + expect(code).to be_present + + jwt_payload = { + iss: @client_id, + sub: @client_id, + aud: api_openid_connect_token_url, + jti: SecureRandom.hex, + exp: 5.minutes.from_now.to_i, + } + + client_assertion = JWT.encode(jwt_payload, client_private_key, 'RS256') + client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' + + page.driver.post api_openid_connect_token_path, + grant_type: 'authorization_code', + code: code, + client_assertion_type: client_assertion_type, + client_assertion: client_assertion + + expect(page.status_code).to eq(200) + token_response = JSON.parse(page.body).with_indifferent_access + + id_token = token_response[:id_token] + expect(id_token).to be_present + + decoded_id_token, _headers = JWT.decode( + id_token, sp_public_key, true, algorithm: 'RS256' + ).map(&:with_indifferent_access) + + sub = decoded_id_token[:sub] + expect(sub).to be_present + expect(decoded_id_token[:nonce]).to eq(@nonce) + expect(decoded_id_token[:aud]).to eq(@client_id) + expect(decoded_id_token[:acr]).to eq(Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF) + expect(decoded_id_token[:iss]).to eq(root_url) + expect(decoded_id_token[:email]).to eq(user.email) + expect(decoded_id_token[:given_name]).to eq('José') + expect(decoded_id_token[:social_security_number]).to eq('666-66-1234') + + access_token = token_response[:access_token] + expect(access_token).to be_present + + page.driver.get api_openid_connect_userinfo_path, + {}, + 'HTTP_AUTHORIZATION' => "Bearer #{access_token}" + + userinfo_response = JSON.parse(page.body).with_indifferent_access + expect(userinfo_response[:sub]).to eq(sub) + expect(userinfo_response[:email]).to eq(user.email) + expect(userinfo_response[:given_name]).to eq('José') + expect(userinfo_response[:social_security_number]).to eq('666-66-1234') + end + end +end + +def client_private_key + @client_private_key ||= begin + OpenSSL::PKey::RSA.new( + File.read(Rails.root.join('keys', 'saml_test_sp.key')) + ) + end +end + +def sp_public_key + page.driver.get api_openid_connect_certs_path + + expect(page.status_code).to eq(200) + certs_response = JSON.parse(page.body).with_indifferent_access + + JSON::JWK.new(certs_response[:keys].first).to_key +end diff --git a/spec/support/idv_examples/usps_verification.rb b/spec/support/idv_examples/usps_verification.rb new file mode 100644 index 00000000000..821499e9d32 --- /dev/null +++ b/spec/support/idv_examples/usps_verification.rb @@ -0,0 +1,45 @@ +shared_examples 'signing in with pending USPS verification' do |sp| + it 'prompts for confirmation code at sign in' do + otp = 'abc123' + profile = create( + :profile, + deactivation_reason: :verification_pending, + phone_confirmed: false, + pii: { otp: otp, ssn: '123-45-6789', dob: '1970-01-01' } + ) + user = profile.user + + visit_idp_from_sp_with_loa3(sp) + + if %i[saml oidc].include?(sp) + sign_in_via_branded_page(user) + else + sign_in_live_with_2fa(user) + end + + expect(current_path).to eq verify_account_path + expect(page).to have_content t('idv.messages.usps.resend') + + fill_in t('forms.verify_profile.name'), with: otp + click_button t('forms.verify_profile.submit') + + expect(user.events.account_verified.size).to eq 1 + expect(page).to_not have_content(t('account.index.verification.reactivate_button')) + + if %i[saml oidc].include?(sp) + expect(current_path).to eq(sign_up_completed_path) + + click_button t('forms.buttons.continue') + + if sp == :saml + expect(current_url).to eq @saml_authn_request + elsif sp == :oidc + redirect_uri = URI(current_url) + + expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') + end + else + expect(current_path).to eq account_path + end + end +end diff --git a/spec/support/idv_examples/usps_verification_selection.rb b/spec/support/idv_examples/usps_verification_selection.rb new file mode 100644 index 00000000000..8254140b306 --- /dev/null +++ b/spec/support/idv_examples/usps_verification_selection.rb @@ -0,0 +1,53 @@ +shared_examples 'selecting usps address verification method' do |sp| + it 'allows the user to select verification via USPS letter', email: true do + visit_idp_from_sp_with_loa3(sp) + + user = register_user + + click_idv_begin + fill_out_idv_form_ok + click_idv_continue + fill_out_financial_form_ok + click_idv_continue + + click_idv_address_choose_usps + + click_on t('idv.buttons.mail.send') + + expect(current_path).to eq verify_review_path + expect(page).to_not have_content t('idv.messages.phone.phone_of_record') + + fill_in :user_password, with: user_password + + expect { click_submit_default }. + to change { UspsConfirmation.count }.from(0).to(1) + + expect(current_path).to eq verify_confirmations_path + click_acknowledge_personal_key + + user.reload + + expect(user.events.account_verified.size).to be(0) + expect(user.profiles.count).to eq 1 + + profile = user.profiles.first + + expect(profile.active?).to eq false + expect(profile.deactivation_reason).to eq 'verification_pending' + expect(profile.phone_confirmed).to eq false + + usps_confirmation_entry = UspsConfirmation.last.decrypted_entry + + expect(current_path).to eq(verify_come_back_later_path) + + if sp == :saml + expect(page).to have_link(t('idv.buttons.return_to_account')) + expect(usps_confirmation_entry.issuer). + to eq('https://rp1.serviceprovider.com/auth/saml/metadata') + elsif sp == :oidc + expect(page).to have_link(t('idv.buttons.return_to_sp', sp: 'Test SP')) + expect(usps_confirmation_entry.issuer). + to eq('urn:gov:gsa:openidconnect:sp:server') + end + end +end diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index fad99ef19ac..4f53f848695 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -161,15 +161,6 @@ def saml_get_auth(settings) get(:auth, SAMLRequest: URI.decode(saml_request(settings))) end - def saml_register_loa3_user(email) - click_link t('sign_up.registrations.create_account') - submit_form_with_valid_email - click_confirmation_link_in_email(email) - submit_form_with_valid_password - set_up_2fa_with_valid_phone - enter_2fa_code - end - private def saml_request(settings) diff --git a/spec/support/shared_examples_for_phone_validation.rb b/spec/support/shared_examples_for_phone_validation.rb index d05b00379f1..a8c3ffc386b 100644 --- a/spec/support/shared_examples_for_phone_validation.rb +++ b/spec/support/shared_examples_for_phone_validation.rb @@ -5,7 +5,8 @@ describe 'phone presence validation' do it 'is invalid when phone is blank' do - subject.submit(phone: '') + params[:phone] = '' + subject.submit(params) expect(subject).to_not be_valid end @@ -27,7 +28,9 @@ end it 'validates that the number matches the requested international code' do - result = subject.submit(phone: '123 123 1234', international_code: 'MA') + 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) @@ -42,7 +45,9 @@ allow(User).to receive(:exists?).with(email: 'new@gmail.com').and_return(false) allow(User).to receive(:exists?).with(phone: second_user.phone).and_return(true) - result = subject.submit(phone: second_user.phone, international_code: 'US') + params[:phone] = second_user.phone + + result = subject.submit(params) expect(result).to be_kind_of(FormResponse) expect(result.success?).to eq(true) end @@ -50,7 +55,7 @@ context 'when phone is not already taken' do it 'is valid' do - result = subject.submit(phone: '+1 (703) 555-1212', international_code: 'US') + result = subject.submit(params) expect(result).to be_kind_of(FormResponse) expect(result.success?).to be true end @@ -58,7 +63,10 @@ context 'when phone is same as current user' do it 'is valid' do - result = subject.submit(phone: user.phone, international_code: 'US') + user.phone = '+1 (555) 500-5000' + params[:phone] = user.phone + result = subject.submit(params) + expect(result).to be_kind_of(FormResponse) expect(result.success?).to be true end @@ -67,7 +75,8 @@ describe '#submit' do it 'formats the phone before assigning it' do - subject.submit(phone: '703-555-1212', international_code: 'US') + params[:phone] = '703-555-1212' + subject.submit(params) expect(subject.phone).to eq '+1 (703) 555-1212' end diff --git a/spec/views/two_factor_authentication_setup/index.html.slim_spec.rb b/spec/views/two_factor_authentication_setup/index.html.slim_spec.rb index dd377cac98c..6cd6180aae2 100644 --- a/spec/views/two_factor_authentication_setup/index.html.slim_spec.rb +++ b/spec/views/two_factor_authentication_setup/index.html.slim_spec.rb @@ -6,7 +6,7 @@ allow(view).to receive(:current_user).and_return(user) - @two_factor_setup_form = TwoFactorSetupForm.new(user) + @user_phone_form = UserPhoneForm.new(user) render end diff --git a/spec/views/users/phones/edit.html.slim_spec.rb b/spec/views/users/phones/edit.html.slim_spec.rb index d82fca5822e..25e42529823 100644 --- a/spec/views/users/phones/edit.html.slim_spec.rb +++ b/spec/views/users/phones/edit.html.slim_spec.rb @@ -5,7 +5,7 @@ before do user = build_stubbed(:user, :signed_up) allow(view).to receive(:current_user).and_return(user) - @update_user_phone_form = UpdateUserPhoneForm.new(user) + @user_phone_form = UserPhoneForm.new(user) end it 'has a localized title' do diff --git a/spec/views/verify/come_back_later/show.html.slim_spec.rb b/spec/views/verify/come_back_later/show.html.slim_spec.rb new file mode 100644 index 00000000000..f3845b2eaa9 --- /dev/null +++ b/spec/views/verify/come_back_later/show.html.slim_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +describe 'verify/come_back_later/show.html.slim' do + let(:sp_return_url) { 'https://www.example.com' } + let(:sp_name) { '🔒🌐💻' } + + before do + decorated_session = instance_double(ServiceProviderSessionDecorator) + allow(decorated_session).to receive(:sp_return_url).and_return(sp_return_url) + allow(decorated_session).to receive(:sp_name).and_return(sp_name) + allow(view).to receive(:decorated_session).and_return(decorated_session) + end + + context 'with an SP with a return url' do + it 'renders a return to SP button' do + render + expect(rendered).to have_link( + t('idv.buttons.return_to_sp', sp: sp_name), + href: sp_return_url + ) + end + end + + context 'with an SP without a return url' do + let(:sp_return_url) { nil } + + it 'renders a return to account button' do + render + expect(rendered).to have_link( + t('idv.buttons.return_to_account', sp: sp_name), + href: account_path + ) + end + end + + context 'without an SP' do + let(:sp_return_url) { nil } + let(:sp_name) { nil } + + it 'renders a return to account button' do + render + expect(rendered).to have_link( + t('idv.buttons.return_to_account', sp: sp_name), + href: account_path + ) + end + end +end