From 9133d68572e63e5412795f858e25285a4548efc0 Mon Sep 17 00:00:00 2001 From: Matt Wagner Date: Thu, 29 Aug 2024 13:52:47 -0400 Subject: [PATCH 1/5] LG-14196 | Set idv_consent_given_at timestamp (#11164) This will be part 1 of a 2-part series. changelog: Internal, Socure, Set idv_consent_given_at on session --- app/controllers/idv/agreement_controller.rb | 1 + app/services/idv/session.rb | 1 + spec/controllers/idv/agreement_controller_spec.rb | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index 17786243597..7537ccc5c6d 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -37,6 +37,7 @@ def update if result.success? idv_session.idv_consent_given = true + idv_session.idv_consent_given_at = Time.zone.now if IdentityConfig.store.in_person_proofing_opt_in_enabled && IdentityConfig.store.in_person_proofing_enabled diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index ef69a96c023..9e76d23f2d6 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -13,6 +13,7 @@ class Session had_barcode_attention_error had_barcode_read_failure idv_consent_given + idv_consent_given_at idv_phone_step_document_capture_session_uuid mail_only_warning_shown opted_in_to_in_person_proofing diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb index 1f090c22136..2eaced54ad0 100644 --- a/spec/controllers/idv/agreement_controller_spec.rb +++ b/spec/controllers/idv/agreement_controller_spec.rb @@ -161,6 +161,12 @@ expect(response).to redirect_to(idv_hybrid_handoff_url) end + + it 'sets an idv_consent_given_at timestamp' do + put :update, params: params + + expect(subject.idv_session.idv_consent_given_at).to be_within(3.seconds).of(Time.zone.now) + end end context 'when both ipp and opt-in ipp are enabled' do @@ -228,6 +234,13 @@ put :update, params: params expect(response).to render_template('idv/agreement/show') end + + it 'does not set IDV consent flags' do + put :update, params: params + + expect(subject.idv_session.idv_consent_given).to be_nil + expect(subject.idv_session.idv_consent_given_at).to be_nil + end end end end From 4915719d00f0727611a909c3788658ed9a5726de Mon Sep 17 00:00:00 2001 From: Doug Price Date: Thu, 29 Aug 2024 16:11:40 -0400 Subject: [PATCH 2/5] LG-14095: make the default doc_auth vendor `mock` (#11162) Previously, the default in the Rails production environment was lexisnexis. This PR makes it mock everywhere, and forces us to explicitly set an overriding default in the S3 configs for each env. [skip changelog] --- config/application.yml.default | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/config/application.yml.default b/config/application.yml.default index 72726ab5de9..323de3318d5 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -97,6 +97,8 @@ doc_auth_max_submission_attempts_before_native_camera: 3 doc_auth_selfie_desktop_test_mode: false doc_auth_separate_pages_enabled: false doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]' +doc_auth_vendor: 'mock' +doc_auth_vendor_default: 'mock' doc_auth_vendor_lexis_nexis_percent: 100 # note, LN is currently the default vendor doc_auth_vendor_socure_percent: 0 doc_auth_vendor_switching_enabled: false @@ -400,8 +402,6 @@ development: dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers doc_auth_selfie_desktop_test_mode: true - doc_auth_vendor: 'mock' - doc_auth_vendor_default: 'mock' domain_name: localhost:3000 enable_rate_limiting: false hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c @@ -461,8 +461,6 @@ production: dashboard_url: https://dashboard.demo.login.gov disable_email_sending: false disable_logout_get_request: false - doc_auth_vendor: 'lexisnexis' - doc_auth_vendor_default: 'lexisnexis' domain_name: login.gov email_registrations_per_ip_track_only_mode: true enable_test_routes: false @@ -517,8 +515,6 @@ test: dashboard_url: https://dashboard.demo.login.gov doc_auth_max_attempts: 4 doc_auth_selfie_desktop_test_mode: true - doc_auth_vendor: 'mock' - doc_auth_vendor_default: 'mock' doc_capture_polling_enabled: false domain_name: www.example.com email_registrations_per_ip_limit: 3 From 3a66710559e1069d7ef7431b542c1bf988e067b3 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:14:47 -0400 Subject: [PATCH 3/5] Revert "LG 13001 Implementation: Select email to share with partner (#10951)" (#11170) This reverts commit 758a8ad096e454d4ecd4a70a21cd12237943f553. --- .../icon_list_item_component.html.erb | 2 +- .../concerns/saml_idp_auth_concern.rb | 9 -- .../authorization_controller.rb | 8 -- .../sign_up/completions_controller.rb | 5 - .../sign_up/select_email_controller.rb | 54 -------- app/forms/openid_connect_authorize_form.rb | 4 +- app/forms/select_email_form.rb | 31 ----- app/models/email_address.rb | 1 - app/models/service_provider_identity.rb | 2 - app/presenters/completions_presenter.rb | 12 +- .../openid_connect_user_info_presenter.rb | 2 +- app/services/attribute_asserter.rb | 12 +- app/services/displayable_pii_formatter.rb | 10 +- app/services/identity_linker.rb | 4 +- app/views/sign_up/completions/show.html.erb | 11 -- app/views/sign_up/select_email/show.html.erb | 50 ------- config/application.yml.default | 2 - config/locales/en.yml | 4 - config/locales/es.yml | 4 - config/locales/fr.yml | 4 - config/locales/zh.yml | 4 - config/routes.rb | 2 - lib/identity_config.rb | 1 - .../sign_up/select_email_controller_spec.rb | 53 ------- .../multiple_emails/sp_sign_in_spec.rb | 106 +------------- spec/forms/delete_user_email_form_spec.rb | 12 -- .../openid_connect_authorize_form_spec.rb | 2 - spec/forms/select_email_form_spec.rb | 60 -------- spec/presenters/completions_presenter_spec.rb | 14 +- ...openid_connect_user_info_presenter_spec.rb | 42 ------ spec/services/attribute_asserter_spec.rb | 130 ------------------ .../displayable_pii_formatter_spec.rb | 10 +- .../sign_up/completions/show.html.erb_spec.rb | 52 +------ .../select_email/show.html.erb_spec.rb | 22 --- 34 files changed, 24 insertions(+), 717 deletions(-) delete mode 100644 app/controllers/sign_up/select_email_controller.rb delete mode 100644 app/forms/select_email_form.rb delete mode 100644 app/views/sign_up/select_email/show.html.erb delete mode 100644 spec/controllers/sign_up/select_email_controller_spec.rb delete mode 100644 spec/forms/select_email_form_spec.rb delete mode 100644 spec/views/sign_up/select_email/show.html.erb_spec.rb diff --git a/app/components/icon_list_item_component.html.erb b/app/components/icon_list_item_component.html.erb index baa4d53d8c7..128a16fccb8 100644 --- a/app/components/icon_list_item_component.html.erb +++ b/app/components/icon_list_item_component.html.erb @@ -2,5 +2,5 @@ <%= content_tag(:div, class: icon_css_class) do %> <%= render IconComponent.new(icon: icon) %> <% end %> -
<%= content %>
+
<%= content %>
<% end %> diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 41b01f10f04..1a055c5cac2 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -140,18 +140,9 @@ def link_identity_from_session_data link_identity( ial: resolved_authn_context_int_ial, rails_session_id: session.id, - email_address_id: email_address_id, ) end - def email_address_id - return nil unless IdentityConfig.store.feature_select_email_to_share_enabled - return user_session[:selected_email_id] if user_session[:selected_email_id].present? - identity = current_user.identities.find_by(service_provider: sp_session['issuer']) - email_id = identity&.email_address_id - return email_id if email_id.is_a? Integer - end - def identity_needs_verification? resolved_authn_context_result.identity_proofing? && current_user.identity_not_verified? end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index f792dd55ac6..faa27028cba 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -84,17 +84,9 @@ def link_identity_to_service_provider current_user: current_user, ial: resolved_authn_context_int_ial, rails_session_id: session.id, - email_address_id: email_address_id, ) end - def email_address_id - return nil unless IdentityConfig.store.feature_select_email_to_share_enabled - return user_session[:selected_email_id] if user_session[:selected_email_id].present? - identity = current_user.identities.find_by(service_provider: sp_session['issuer']) - identity&.email_address_id - end - def ial_context IalContext.new( ial: resolved_authn_context_int_ial, diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 007e1609c95..1107fa56309 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -21,10 +21,6 @@ def update track_completion_event('agency-page') update_verified_attributes send_in_person_completion_survey - if user_session[:selected_email_id].nil? - user_session[:selected_email_id] = EmailContext.new(current_user). - last_sign_in_email_address.id - end if decider.go_back_to_mobile_app? sign_user_out_and_instruct_to_go_back_to_mobile_app else @@ -53,7 +49,6 @@ def completions_presenter requested_attributes: decorated_sp_session.requested_attributes.map(&:to_sym), ial2_requested: ial2_requested?, completion_context: needs_completion_screen_reason, - selected_email_id: user_session[:selected_email_id], ) end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb deleted file mode 100644 index 2c6b52a2382..00000000000 --- a/app/controllers/sign_up/select_email_controller.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module SignUp - class SelectEmailController < ApplicationController - before_action :confirm_two_factor_authenticated - before_action :verify_needs_completions_screen - - def show - @sp_name = current_sp.friendly_name || sp.agency&.name - @user_emails = user_emails - @last_sign_in_email_address = last_email - @select_email_form = build_select_email_form - end - - def create - @select_email_form = build_select_email_form - - result = @select_email_form.submit(form_params) - if result.success? - user_session[:selected_email_id] = form_params[:selected_email_id] - redirect_to sign_up_completed_path - else - flash[:error] = result.first_error_message - redirect_to sign_up_select_email_path - end - end - - def user_emails - @user_emails = current_user.confirmed_email_addresses - end - - private - - def build_select_email_form - SelectEmailForm.new(current_user) - end - - def form_params - params.fetch(:select_email_form, {}).permit(:selected_email_id) - end - - def last_email - if user_session[:selected_email_id] - user_emails.find(user_session[:selected_email_id]).email - else - EmailContext.new(current_user).last_sign_in_email_address.email - end - end - - def verify_needs_completions_screen - redirect_to account_url unless needs_completion_screen_reason - end - end -end diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index bbe816167a7..a70dcf8d09d 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -94,8 +94,7 @@ def service_provider def link_identity_to_service_provider( current_user:, ial:, - rails_session_id:, - email_address_id: + rails_session_id: ) identity_linker = IdentityLinker.new(current_user, service_provider) @identity = identity_linker.link_identity( @@ -107,7 +106,6 @@ def link_identity_to_service_provider( requested_aal_value: requested_aal_value, scope: scope.join(' '), code_challenge: code_challenge, - email_address_id: email_address_id, ) end diff --git a/app/forms/select_email_form.rb b/app/forms/select_email_form.rb deleted file mode 100644 index 165d5e0f331..00000000000 --- a/app/forms/select_email_form.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class SelectEmailForm - include ActiveModel::Model - include ActionView::Helpers::TranslationHelper - - attr_reader :user, :selected_email_id - - validate :validate_owns_selected_email - - def initialize(user) - @user = user - end - - def submit(params) - @selected_email_id = params[:selected_email_id] - - success = valid? - FormResponse.new(success:, errors:) - end - - private - - def validate_owns_selected_email - return if user.confirmed_email_addresses.exists?(id: selected_email_id) - - errors.add :email, I18n.t( - 'email_address.not_found', - ), type: :selected_email_id - end -end diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 1692826343d..b4d1f28fc3c 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -11,7 +11,6 @@ class EmailAddress < ApplicationRecord # rubocop:disable Rails/HasManyOrHasOneDependent has_one :suspended_email # rubocop:enable Rails/HasManyOrHasOneDependent - has_many :identities, class_name: 'ServiceProviderIdentity', dependent: :nullify scope :confirmed, -> { where('confirmed_at IS NOT NULL') } diff --git a/app/models/service_provider_identity.rb b/app/models/service_provider_identity.rb index 5030aea024c..dd17e94a0f3 100644 --- a/app/models/service_provider_identity.rb +++ b/app/models/service_provider_identity.rb @@ -19,8 +19,6 @@ class ServiceProviderIdentity < ApplicationRecord # rubocop:enable Rails/InverseOf has_one :agency, through: :service_provider_record - belongs_to :email_address - scope :not_deleted, -> { where(deleted_at: nil) } CONSENT_EXPIRATION = 1.year.freeze diff --git a/app/presenters/completions_presenter.rb b/app/presenters/completions_presenter.rb index 55f8cadb77a..8e5b831624b 100644 --- a/app/presenters/completions_presenter.rb +++ b/app/presenters/completions_presenter.rb @@ -4,8 +4,7 @@ class CompletionsPresenter include ActionView::Helpers::TranslationHelper include ActionView::Helpers::TagHelper - attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, - :completion_context, :selected_email_id + attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, :completion_context SORTED_IAL2_ATTRIBUTE_MAPPING = [ [[:email], :email], @@ -34,8 +33,7 @@ def initialize( decrypted_pii:, requested_attributes:, ial2_requested:, - completion_context:, - selected_email_id: + completion_context: ) @current_user = current_user @current_sp = current_sp @@ -43,7 +41,6 @@ def initialize( @requested_attributes = requested_attributes @ial2_requested = ial2_requested @completion_context = completion_context - @selected_email_id = selected_email_id end def ial2_requested? @@ -105,10 +102,6 @@ def pii end end - def multiple_emails? - current_user.confirmed_email_addresses.many? - end - private def first_time_signing_in? @@ -119,7 +112,6 @@ def displayable_pii @displayable_pii ||= DisplayablePiiFormatter.new( current_user: current_user, pii: decrypted_pii, - selected_email_id: @selected_email_id, ).format end diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index bc87e20231b..1fd4f925e22 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -54,7 +54,7 @@ def uuid_from_sp_identity(identity) end def email_from_sp_identity - identity.email_address&.email || email_context.last_sign_in_email_address.email + email_context.last_sign_in_email_address.email end def all_emails_from_sp_identity(identity) diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index d2320b77b10..125a720d3be 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -200,10 +200,7 @@ def attribute_getter_function_ascii(attr) def add_email(attrs) attrs[:email] = { - getter: ->(principal) { - last_email_from_sp(principal) || - EmailContext.new(principal).last_sign_in_email_address.email - }, + getter: ->(principal) { EmailContext.new(principal).last_sign_in_email_address.email }, name_format: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', name_id_format: Saml::XML::Namespaces::Formats::NameId::EMAIL_ADDRESS, } @@ -217,13 +214,6 @@ def add_all_emails(attrs) } end - def last_email_from_sp(principal) - return nil unless IdentityConfig.store.feature_select_email_to_share_enabled - identity = principal.active_identity_for(service_provider) - email_id = identity&.email_address_id - principal.confirmed_email_addresses.find_by(id: email_id)&.email if email_id - end - def bundle @bundle ||= ( authn_request_bundle || service_provider.metadata[:attribute_bundle] || [] diff --git a/app/services/displayable_pii_formatter.rb b/app/services/displayable_pii_formatter.rb index 1bc6399fd0b..a9c61ad1580 100644 --- a/app/services/displayable_pii_formatter.rb +++ b/app/services/displayable_pii_formatter.rb @@ -8,12 +8,10 @@ class DisplayablePiiFormatter attr_reader :current_user attr_reader :pii - attr_reader :selected_email_id - def initialize(current_user:, pii:, selected_email_id:) + def initialize(current_user:, pii:) @current_user = current_user @pii = pii - @selected_email_id = selected_email_id end # @return [FormattedPii] @@ -38,11 +36,7 @@ def format private def email - if @selected_email_id - current_user.confirmed_email_addresses.find(@selected_email_id).email - else - EmailContext.new(current_user).last_sign_in_email_address.email - end + EmailContext.new(current_user).last_sign_in_email_address.email end def all_emails diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index 8f69f422560..b34f76077f7 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -25,8 +25,7 @@ def link_identity( scope: nil, verified_attributes: nil, last_consented_at: nil, - clear_deleted_at: nil, - email_address_id: nil + clear_deleted_at: nil ) return unless user && service_provider.present? @@ -44,7 +43,6 @@ def link_identity( rails_session_id: rails_session_id, scope: scope, verified_attributes: combined_verified_attributes(verified_attributes), - email_address_id: email_address_id, ).tap do |hash| hash[:last_consented_at] = last_consented_at if last_consented_at hash[:deleted_at] = nil if clear_deleted_at diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index e019e17a33d..6dbe1572268 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -42,17 +42,6 @@ last_number: attribute_value[-1], ), ) %> - <% elsif attribute_key == :email && IdentityConfig.store.feature_select_email_to_share_enabled %> -
- <%= attribute_value.to_s %> -

- <% if @presenter.multiple_emails? %> - <%= link_to t('help_text.requested_attributes.change_email_link'), sign_up_select_email_path %> - <% else %> - <%= link_to t('account.index.email_add'), add_email_path %> - <% end %> -

-
<% else %> <%= attribute_value.to_s %> <% end %> diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb deleted file mode 100644 index c89ce9a7676..00000000000 --- a/app/views/sign_up/select_email/show.html.erb +++ /dev/null @@ -1,50 +0,0 @@ -<% self.title = t('titles.select_email') %> - -<%= render StatusPageComponent.new(status: :info, icon: :question) do |c| %> - <% c.with_header { t('titles.select_email') } %> -

- <%= I18n.t('help_text.select_preferred_email', sp: @sp_name, app_name: APP_NAME) %> -

- - <%= simple_form_for('', url: sign_up_select_email_path) do |f| %> -
-
-
-
- <% @user_emails.each do |email, index| %> -
- <%= radio_button_tag( - 'select_email_form[selected_email_id]', - email.id, - email.email == @last_sign_in_email_address, - class: 'usa-radio__input usa-radio__input--bordered', - ) %> - <%= label_tag( - "select_email_form_selected_email_id_#{email.id}", - class: 'usa-radio__label width-full', - ) do %> - <%= email.email %> - <% end %> -
- <% end %> -
-
-
-
-
- <%= f.submit t('help_text.requested_attributes.change_email_link'), class: 'margin-top-4' %> - <% end %> - - <%= render ButtonComponent.new( - url: add_email_path, - outline: true, - big: true, - wide: true, - class: 'margin-top-2', - ).with_content(t('account.index.email_add')) %> - - <%= render PageFooterComponent.new do %> - <%= link_to t('forms.buttons.back'), sign_up_completed_path %> - <% end %> - -<% end %> \ No newline at end of file diff --git a/config/application.yml.default b/config/application.yml.default index 323de3318d5..039fd0d3300 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -118,7 +118,6 @@ enable_usps_verification: true event_disavowal_expiration_hours: 240 feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true -feature_select_email_to_share_enabled: true geo_data_file_path: 'geo_data/GeoLite2-City.mmdb' get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 @@ -465,7 +464,6 @@ production: email_registrations_per_ip_track_only_mode: true enable_test_routes: false enable_usps_verification: false - feature_select_email_to_share_enabled: false hmac_fingerprinter_key: hmac_fingerprinter_key_queue: '[]' idv_sp_required: true diff --git a/config/locales/en.yml b/config/locales/en.yml index 9dca9c10a6c..1e46caae24f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -692,7 +692,6 @@ doc_auth.tips.review_issues_id_text1: Did you use a dark background? doc_auth.tips.review_issues_id_text2: Did you take the photo on a flat surface? doc_auth.tips.review_issues_id_text3: Is the flash on your camera off? doc_auth.tips.review_issues_id_text4: Are all details sharp and clearly visible? -email_address.not_found: 'Email not found' email_addresses.add.duplicate: This email address is already registered to your account. email_addresses.add.limit: You’ve added the maximum number of email addresses. email_addresses.delete.bullet1: You won’t be able to sign in to %{app_name} (or any of the government applications linked to your account) using this email address @@ -959,7 +958,6 @@ headings.webauthn_setup.new: Insert your security key help_text.requested_attributes.address: Address help_text.requested_attributes.all_emails: Email addresses on your account help_text.requested_attributes.birthdate: Date of birth -help_text.requested_attributes.change_email_link: Change help_text.requested_attributes.consent_reminder_html: You must consent each year to share your information with %{sp_html}. help_text.requested_attributes.email: Email address help_text.requested_attributes.full_name: Full name @@ -970,7 +968,6 @@ help_text.requested_attributes.social_security_number: Social Security number help_text.requested_attributes.verified_at: Updated on help_text.requested_attributes.x509_issuer: PIV/CAC Issuer help_text.requested_attributes.x509_subject: PIV/CAC Identity -help_text.select_preferred_email: You may change which email you share with %{sp} since you have multiple emails associated with your %{app_name} account. i18n.language: Language i18n.locale.en: English i18n.locale.es: Español @@ -1607,7 +1604,6 @@ titles.reactivate_account: Reactivate your account titles.registrations.new: Create your account titles.revoke_consent: Revoke Consent titles.rules_of_use: Rules of Use -titles.select_email: Select your preferred email titles.sign_up.completion_consent_expired_ial1: It’s been a year since you gave us consent to share your information titles.sign_up.completion_consent_expired_ial2: It’s been a year since you gave us consent to share your verified identity titles.sign_up.completion_first_sign_in: Continue to %{sp} diff --git a/config/locales/es.yml b/config/locales/es.yml index de9a55b97fb..b6807a59895 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -703,7 +703,6 @@ doc_auth.tips.review_issues_id_text1: '¿Usó un fondo de color oscuro?' doc_auth.tips.review_issues_id_text2: '¿Tomó la foto en una superficie plana?' doc_auth.tips.review_issues_id_text3: '¿Está apagado el flash de su cámara?' doc_auth.tips.review_issues_id_text4: '¿Todos los detalles se ven con precisión y claridad?' -email_address.not_found: El correo electrónico no encontrado email_addresses.add.duplicate: Esta dirección de correo electrónico ya está registrada en su cuenta. email_addresses.add.limit: Agregó el número máximo de direcciones de correo electrónico. email_addresses.delete.bullet1: Si usa esta dirección de correo electrónico, no podrá iniciar sesión en %{app_name} (ni en ninguna de las aplicaciones gubernamentales vinculadas a su cuenta). @@ -970,7 +969,6 @@ headings.webauthn_setup.new: Inserte su clave de seguridad help_text.requested_attributes.address: Dirección help_text.requested_attributes.all_emails: Direcciones de correo electrónico en su cuenta help_text.requested_attributes.birthdate: Fecha de nacimiento -help_text.requested_attributes.change_email_link: Cambiar help_text.requested_attributes.consent_reminder_html: Debe dar su consentimiento cada año para divulgar su información a %{sp_html}. help_text.requested_attributes.email: Dirección de correo electrónico help_text.requested_attributes.full_name: Nombre completo @@ -981,7 +979,6 @@ help_text.requested_attributes.social_security_number: Número de Seguro Social help_text.requested_attributes.verified_at: Actualizado en help_text.requested_attributes.x509_issuer: Emisor de la tarjeta PIV o CAC help_text.requested_attributes.x509_subject: Identidad de la tarjeta PIV o CAC -help_text.select_preferred_email: Puede cambiar el correo electrónico que comparte con %{sp} ya que tiene varios correos electrónicos asociados a su cuenta de %{app_name}. i18n.language: Idioma i18n.locale.en: English i18n.locale.es: Español @@ -1619,7 +1616,6 @@ titles.reactivate_account: Reactive su cuenta titles.registrations.new: Cree su cuenta titles.revoke_consent: Revocar consentimiento titles.rules_of_use: Reglas de uso -titles.select_email: Seleccione el correo electrónico que prefiera titles.sign_up.completion_consent_expired_ial1: Hace un año que nos dio su consentimiento para divulgar su información titles.sign_up.completion_consent_expired_ial2: Hace un año que nos dio su consentimiento para divulgar su identidad verificada titles.sign_up.completion_first_sign_in: Continuar con %{sp} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2cf64287c9b..9a9665b2fc2 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -692,7 +692,6 @@ doc_auth.tips.review_issues_id_text1: Avez-vous utilisé un arrière-plan de cou doc_auth.tips.review_issues_id_text2: Avez-vous pris la photo sur une surface plane ? doc_auth.tips.review_issues_id_text3: Le flash de votre appareil photo est-il éteint ? doc_auth.tips.review_issues_id_text4: Tous les détails sont-ils nets et clairement visibles ? -email_address.not_found: Email non trouvé email_addresses.add.duplicate: Cette adresse e-mail est déjà enregistrée sur votre compte. email_addresses.add.limit: Vous avez ajouté le nombre maximum d’adresses e-mail. email_addresses.delete.bullet1: Vous ne pourrez pas vous connecter à %{app_name} (ni à aucune des applications de l’administration associées à votre compte) à l’aide de cette adresse e-mail @@ -959,7 +958,6 @@ headings.webauthn_setup.new: Insérer votre clé de sécurité help_text.requested_attributes.address: Adresse help_text.requested_attributes.all_emails: Adresses e-mail sur votre compte help_text.requested_attributes.birthdate: Date de naissance -help_text.requested_attributes.change_email_link: Modifier help_text.requested_attributes.consent_reminder_html: Vous devez consentir chaque année au partage de vos informations avec %{sp_html}. help_text.requested_attributes.email: Adresse e-mail help_text.requested_attributes.full_name: Nom complet @@ -970,7 +968,6 @@ help_text.requested_attributes.social_security_number: Numéro de sécurité soc help_text.requested_attributes.verified_at: Mis à jour le help_text.requested_attributes.x509_issuer: Émetteur PIV/CAC help_text.requested_attributes.x509_subject: Identité PIV/CAC -help_text.select_preferred_email: Vous pouvez modifier l’adresse e-mail que vous partagez avec %{sp} car vous possédez plusieurs adresses e-mail associées à votre compte %{app_name}. i18n.language: Langue i18n.locale.en: English i18n.locale.es: Español @@ -1607,7 +1604,6 @@ titles.reactivate_account: Réactiver votre compte titles.registrations.new: Créer votre compte titles.revoke_consent: Révoquer le consentement titles.rules_of_use: Règles d’utilisation -titles.select_email: Sélectionner l’adresse e-mail de votre choix titles.sign_up.completion_consent_expired_ial1: Cela fait un an que vous nous avez donné votre consentement pour partager vos informations titles.sign_up.completion_consent_expired_ial2: Cela fait un an que vous nous avez donné votre consentement pour partager votre identité vérifiée titles.sign_up.completion_first_sign_in: Continuer vers %{sp} diff --git a/config/locales/zh.yml b/config/locales/zh.yml index d2c92c4f34b..1e6cb2d59f3 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -703,7 +703,6 @@ doc_auth.tips.review_issues_id_text1: 你是否使用了暗色背景? doc_auth.tips.review_issues_id_text2: 是否在平坦平面上拍的照? doc_auth.tips.review_issues_id_text3: 你相机的闪光灯是否关闭? doc_auth.tips.review_issues_id_text4: 是否所有细节都清晰可见? -email_address.not_found: 未找到电子邮件 email_addresses.add.duplicate: 该电邮地址已注册到你的账户。 email_addresses.add.limit: 你添加的电邮地址数目已达最多。 email_addresses.delete.bullet1: 使用该电邮地址你无法登录进入 %{app_name} (或任何其他与你账户关联的政府应用程序)。 @@ -972,7 +971,6 @@ headings.webauthn_setup.new: 插入您的安全密钥 help_text.requested_attributes.address: 地址 help_text.requested_attributes.all_emails: 你账户上的电邮地址 help_text.requested_attributes.birthdate: 生日 -help_text.requested_attributes.change_email_link: 更改 help_text.requested_attributes.consent_reminder_html: 你每年都必须授权同意与 %{sp_html} 分享信息。 help_text.requested_attributes.email: 电邮地址 help_text.requested_attributes.full_name: 姓名 @@ -983,7 +981,6 @@ help_text.requested_attributes.social_security_number: 社会保障号码 help_text.requested_attributes.verified_at: 更新是在 help_text.requested_attributes.x509_issuer: PIV/CAC 发放方 help_text.requested_attributes.x509_subject: PIV/CAC 身份 -help_text.select_preferred_email: 因为你有多个电邮与 %{app_name} 账户相关,你可以更改与我们 %{sp} 构分享哪个。 i18n.language: 语言 i18n.locale.en: English i18n.locale.es: Español @@ -1620,7 +1617,6 @@ titles.reactivate_account: 重新激活你账户 titles.registrations.new: 设立账户 titles.revoke_consent: 撤销同意 titles.rules_of_use: 使用规则 -titles.select_email: 选择你比较愿意分享的电邮 titles.sign_up.completion_consent_expired_ial1: 从你上次授权我们分享你的信息已经一年了。 titles.sign_up.completion_consent_expired_ial2: 从你上次授权我们分享你验证过的身份已经一年了。 titles.sign_up.completion_first_sign_in: 继续到 %{sp} diff --git a/config/routes.rb b/config/routes.rb index eb35a338ee7..30863b3e145 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -304,8 +304,6 @@ get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register get '/sign_up/enter_password' => 'sign_up/passwords#new' - get '/sign_up/select_email' => 'sign_up/select_email#show' - post '/sign_up/select_email' => 'sign_up/select_email#create' get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed post '/sign_up/completed' => 'sign_up/completions#update' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 246d4210f73..cfa409bbd2f 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -138,7 +138,6 @@ def self.store config.add(:event_disavowal_expiration_hours, type: :integer) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) - config.add(:feature_select_email_to_share_enabled, type: :boolean) config.add(:geo_data_file_path, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb deleted file mode 100644 index 898c402091d..00000000000 --- a/spec/controllers/sign_up/select_email_controller_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'rails_helper' - -RSpec.describe SignUp::SelectEmailController do - describe 'before_actions' do - it 'requires the user be logged in and authenticated' do - expect(subject).to have_actions( - :before, - :confirm_two_factor_authenticated, - ) - end - - it 'requires the user be in the completions flow' do - expect(subject).to have_actions( - :before, - :verify_needs_completions_screen, - ) - end - end - - describe '#create' do - let(:email) { 'michael.motorist@email.com' } - let(:email2) { 'michael.motorist2@email.com' } - let(:email3) { 'david.motorist@email.com' } - let(:user) { create(:user) } - - before do - user.email_addresses = [] - create(:email_address, user:, email: email) - create(:email_address, user:, email: email2) - end - - it 'updates selected email address' do - post :create, params: { selected_email_id: email2 } - - expect(user.email_addresses.last.email). - to include('michael.motorist2@email.com') - end - - context 'with a corrupted email selected_email_id form' do - render_views - it 'rejects email not belonging to the user' do - stub_sign_in(user) - allow(controller).to receive(:needs_completion_screen_reason).and_return(true) - post :create, params: { selected_email_id: email3 } - - expect(user.email_addresses.last.email). - to include('michael.motorist2@email.com') - - expect(response).to redirect_to(sign_up_select_email_path) - end - end - end -end diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb index be6d75b55b6..dbae13eb376 100644 --- a/spec/features/multiple_emails/sp_sign_in_spec.rb +++ b/spec/features/multiple_emails/sp_sign_in_spec.rb @@ -17,62 +17,15 @@ fill_in_code_with_last_phone_otp click_submit_default click_agree_and_continue if current_path == sign_up_completed_path + decoded_id_token = fetch_oidc_id_token_info - expect(decoded_id_token[:email]).to eq(emails.first) + expect(decoded_id_token[:email]).to eq(email) expect(decoded_id_token[:all_emails]).to be_nil Capybara.reset_session! end end - scenario 'signing in with OIDC and selecting an alternative email address at first sign in' do - user = create(:user, :fully_registered, :with_multiple_emails) - emails = user.reload.email_addresses.map(&:email) - - visit_idp_from_oidc_sp(scope: 'openid email') - signin(emails.first, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - click_link(t('help_text.requested_attributes.change_email_link')) - - choose emails.second - - click_button(t('help_text.requested_attributes.change_email_link')) - - expect(current_path).to eq(sign_up_completed_path) - click_agree_and_continue - decoded_id_token = fetch_oidc_id_token_info - expect(decoded_id_token[:email]).to eq(emails.second) - end - - scenario 'signing in with OIDC after deleting email linked to identity' do - user = create(:user, :fully_registered) - email1 = create(:email_address, user:, email: 'email1@example.com') - email2 = create(:email_address, user:, email: 'email2@example.com') - - # Link identity with email - visit_idp_from_oidc_sp(scope: 'openid email') - signin(email1.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - click_link(t('help_text.requested_attributes.change_email_link')) - choose email2.email - click_button(t('help_text.requested_attributes.change_email_link')) - expect(current_path).to eq(sign_up_completed_path) - click_agree_and_continue - click_submit_default - - # Delete email from account - visit manage_email_confirm_delete_url(id: email2.id) - click_button t('forms.email.buttons.delete') - - # Sign in again to partner application - visit_idp_from_oidc_sp(scope: 'openid email') - - decoded_id_token = fetch_oidc_id_token_info - expect(decoded_id_token[:email]).to eq(email1.email) - end - scenario 'signing in with SAML sends the email address used to sign in' do user = create(:user, :fully_registered, :with_multiple_emails) emails = user.reload.email_addresses.map(&:email) @@ -89,63 +42,12 @@ xmldoc = SamlResponseDoc.new('feature', 'response_assertion') email_from_saml_response = xmldoc.attribute_value_for('email') - expect(email_from_saml_response).to eq(emails.first) + + expect(email_from_saml_response).to eq(email) Capybara.reset_session! end end - - scenario 'signing in with SAML and selecting an alternative email address at first sign in' do - user = create(:user, :fully_registered, :with_multiple_emails) - emails = user.reload.email_addresses.map(&:email) - - visit authn_request - signin(emails.first, user.password) - fill_in_code_with_last_phone_otp - click_submit_default_twice - - click_link(t('help_text.requested_attributes.change_email_link')) - choose emails.second - click_button(t('help_text.requested_attributes.change_email_link')) - - expect(current_path).to eq(sign_up_completed_path) - - click_agree_and_continue - click_submit_default - - xmldoc = SamlResponseDoc.new('feature', 'response_assertion') - email_from_saml_response = xmldoc.attribute_value_for('email') - expect(email_from_saml_response).to eq(emails.second) - end - - scenario 'signing in with SAML after deleting email linked to identity' do - user = create(:user, :fully_registered) - email1 = create(:email_address, user:, email: 'email1@example.com') - email2 = create(:email_address, user:, email: 'email2@example.com') - - # Link identity with email - visit authn_request - signin(email1.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default_twice - click_link(t('help_text.requested_attributes.change_email_link')) - choose email2.email - click_button(t('help_text.requested_attributes.change_email_link')) - expect(current_path).to eq(sign_up_completed_path) - click_agree_and_continue - click_submit_default - - # Delete email from account - visit manage_email_confirm_delete_url(id: email2.id) - click_button t('forms.email.buttons.delete') - - # Sign in again to partner application - visit authn_request - - xmldoc = SamlResponseDoc.new('feature', 'response_assertion') - email_from_saml_response = xmldoc.attribute_value_for('email') - expect(email_from_saml_response).to eq(email1.email) - end end context 'with the all_emails scope' do diff --git a/spec/forms/delete_user_email_form_spec.rb b/spec/forms/delete_user_email_form_spec.rb index 68e9fbd88b6..8bc089fa4b6 100644 --- a/spec/forms/delete_user_email_form_spec.rb +++ b/spec/forms/delete_user_email_form_spec.rb @@ -60,18 +60,6 @@ submit end - - it 'removes associated identity email address id' do - user.identities << ServiceProviderIdentity.create( - service_provider: 'http://localhost:3000', - last_authenticated_at: Time.zone.now, - ) - user.identities.last.email_address_id = email_address.id - - submit - - expect(user.identities.last.email_address_id).to be(nil) - end end context 'with a email of a different user' do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 580983f27d7..568c2553535 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -661,7 +661,6 @@ current_user: user, ial: 1, rails_session_id: rails_session_id, - email_address_id: 4, ) identity = user.identities.where(service_provider: client_id).first @@ -685,7 +684,6 @@ current_user: user, ial: 1, rails_session_id: rails_session_id, - email_address_id: 4, ) end diff --git a/spec/forms/select_email_form_spec.rb b/spec/forms/select_email_form_spec.rb deleted file mode 100644 index 23a17bdbba5..00000000000 --- a/spec/forms/select_email_form_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'rails_helper' - -RSpec.describe SelectEmailForm do - let(:user) { create(:user, :fully_registered, :with_multiple_emails) } - describe '#submit' do - it 'returns the email successfully' do - form = SelectEmailForm.new(user) - response = form.submit(selected_email_id: user.email_addresses.last.id) - - expect(response.success?).to eq(true) - end - - it 'returns an error when submitting an invalid email' do - form = SelectEmailForm.new(user) - response = form.submit(selected_email_id: nil) - - expect(response.success?).to eq(false) - end - - context 'with an unconfirmed email address added' do - before do - create( - :email_address, - email: 'michael.business@business.com', - user: user, - confirmed_at: nil, - confirmation_sent_at: 1.month.ago, - ) - end - - it 'returns an error' do - form = SelectEmailForm.new(user) - response = form.submit(selected_email_id: user.email_addresses.last.id) - - expect(response.success?).to eq(false) - end - end - - context 'with another user\'s email' do - let(:user2) { create(:user, :fully_registered, :with_multiple_emails) } - before do - create( - :email_address, - email: 'michael.business@business.com', - user: user2, - confirmed_at: nil, - confirmation_sent_at: 1.month.ago, - ) - @email2 = user2.email_addresses.last.id - end - - it 'returns an error' do - form = SelectEmailForm.new(user) - response = form.submit(selected_email_id: @email2) - - expect(response.success?).to eq(false) - end - end - end -end diff --git a/spec/presenters/completions_presenter_spec.rb b/spec/presenters/completions_presenter_spec.rb index 132dd2fe5e8..07c32b8deb1 100644 --- a/spec/presenters/completions_presenter_spec.rb +++ b/spec/presenters/completions_presenter_spec.rb @@ -15,7 +15,6 @@ end let(:current_user) { create(:user, :fully_registered, identities: identities) } let(:current_sp) { create(:service_provider, friendly_name: 'Friendly service provider') } - let(:selected_email_id) { current_user.email_addresses.first.id } let(:decrypted_pii) do Pii::Attributes.new( first_name: 'Testy', @@ -42,13 +41,12 @@ subject(:presenter) do described_class.new( - current_user:, - current_sp:, - decrypted_pii:, - requested_attributes:, - ial2_requested:, - completion_context:, - selected_email_id:, + current_user: current_user, + current_sp: current_sp, + decrypted_pii: decrypted_pii, + requested_attributes: requested_attributes, + ial2_requested: ial2_requested, + completion_context: completion_context, ) end diff --git a/spec/presenters/openid_connect_user_info_presenter_spec.rb b/spec/presenters/openid_connect_user_info_presenter_spec.rb index 8aa2bfecce4..e22176e0329 100644 --- a/spec/presenters/openid_connect_user_info_presenter_spec.rb +++ b/spec/presenters/openid_connect_user_info_presenter_spec.rb @@ -368,47 +368,5 @@ end end end - - context 'with a deleted email' do - let(:identity) do - build( - :service_provider_identity, - rails_session_id: rails_session_id, - user: create(:user, :fully_registered, :with_multiple_emails), - scope: scope, - ) - end - - before do - identity.email_address_id = identity.user.email_addresses.first.id - identity.user.email_addresses.first.delete - end - - it 'defers to user alternate email' do - expect(identity.user.reload.email_addresses.first.id). - to_not eq(identity.email_address_id) - expect(identity.user.reload.email_addresses.count).to be 1 - expect(user_info[:email]).to eq(identity.user.email_addresses.last.email) - end - end - - context 'with nil email id' do - let(:identity) do - build( - :service_provider_identity, - rails_session_id: rails_session_id, - user: create(:user, :fully_registered), - scope: scope, - ) - end - - before do - identity.email_address_id = nil - end - - it 'adds the signed in email id to the identity' do - expect(user_info[:email]).to eq(identity.user.email_addresses.last.email) - end - end end end diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index f8e656f3e75..5ad52b0a9c0 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -717,136 +717,6 @@ it_behaves_like 'unverified user' end - - context 'with a deleted email' do - let(:subject) do - described_class.new( - user: user, - name_id_format: name_id_format, - service_provider: service_provider, - authn_request: authn_request, - decrypted_pii: decrypted_pii, - user_session: user_session, - ) - end - before do - user.identities << identity - allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). - and_return(%w[email phone first_name]) - create(:email_address, user:, email: 'email@example.com') - - ident = user.identities.last - ident.email_address_id = user.email_addresses.first.id - ident.save - subject.build - - user.email_addresses.first.delete - - subject.build - end - - it 'defers to user alternate email' do - expect(get_asserted_attribute(user, :email)). - to eq 'email@example.com' - end - end - - context 'with a nil email id' do - let(:subject) do - described_class.new( - user: user, - name_id_format: name_id_format, - service_provider: service_provider, - authn_request: authn_request, - decrypted_pii: decrypted_pii, - user_session: user_session, - ) - end - before do - user.identities << identity - allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). - and_return(%w[email phone first_name]) - - ident = user.identities.last - ident.email_address_id = nil - ident.save - subject.build - end - - it 'defers to user alternate email' do - expect(get_asserted_attribute(user, :email)). - to eq user.email_addresses.last.email - end - end - - context 'select email to send to partner feature is disabled' do - before do - allow(IdentityConfig.store).to receive( - :feature_select_email_to_share_enabled, - ).and_return(false) - end - - context 'with a deleted email' do - let(:subject) do - described_class.new( - user: user, - name_id_format: name_id_format, - service_provider: service_provider, - authn_request: authn_request, - decrypted_pii: decrypted_pii, - user_session: user_session, - ) - end - before do - user.identities << identity - allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). - and_return(%w[email phone first_name]) - create(:email_address, user:, email: 'email@example.com') - - ident = user.identities.last - ident.email_address_id = user.email_addresses.first.id - ident.save - subject.build - - user.email_addresses.first.delete - - subject.build - end - - it 'defers to user alternate email' do - expect(get_asserted_attribute(user, :email)). - to eq 'email@example.com' - end - end - - context 'with a nil email id' do - let(:subject) do - described_class.new( - user: user, - name_id_format: name_id_format, - service_provider: service_provider, - authn_request: authn_request, - decrypted_pii: decrypted_pii, - user_session: user_session, - ) - end - before do - user.identities << identity - allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). - and_return(%w[email phone first_name]) - - ident = user.identities.last - ident.email_address_id = nil - ident.save - subject.build - end - - it 'defers to user alternate email' do - expect(get_asserted_attribute(user, :email)). - to eq user.email_addresses.last.email - end - end - end end describe 'aal attributes handling' do diff --git a/spec/services/displayable_pii_formatter_spec.rb b/spec/services/displayable_pii_formatter_spec.rb index e2f6aa407bf..b62885eaeb9 100644 --- a/spec/services/displayable_pii_formatter_spec.rb +++ b/spec/services/displayable_pii_formatter_spec.rb @@ -48,8 +48,6 @@ ) end - let(:selected_email_id) { current_user.email_addresses.first.id } - let(:pii) do { first_name: first_name, @@ -65,13 +63,7 @@ } end - subject(:formatter) do - described_class.new( - current_user:, - pii:, - selected_email_id:, - ) - end + subject(:formatter) { described_class.new(current_user: current_user, pii: pii) } describe '#format' do context 'ial1' do diff --git a/spec/views/sign_up/completions/show.html.erb_spec.rb b/spec/views/sign_up/completions/show.html.erb_spec.rb index 352e3e293e1..ee045ce15f1 100644 --- a/spec/views/sign_up/completions/show.html.erb_spec.rb +++ b/spec/views/sign_up/completions/show.html.erb_spec.rb @@ -3,7 +3,6 @@ RSpec.describe 'sign_up/completions/show.html.erb' do let(:user) { create(:user, :fully_registered) } let(:service_provider) { create(:service_provider) } - let(:selected_email_id) { user.email_addresses.first.id } let(:decrypted_pii) { {} } let(:requested_attributes) { [:email] } let(:ial2_requested) { false } @@ -23,11 +22,10 @@ CompletionsPresenter.new( current_user: user, current_sp: service_provider, - decrypted_pii:, - requested_attributes:, - ial2_requested:, - completion_context:, - selected_email_id:, + decrypted_pii: decrypted_pii, + requested_attributes: requested_attributes, + ial2_requested: ial2_requested, + completion_context: completion_context, ) end @@ -61,48 +59,6 @@ ) end - context 'select email to send to partner and select email feature is disabled' do - before do - allow(IdentityConfig.store).to receive( - :feature_select_email_to_share_enabled, - ).and_return(false) - end - - it 'does not show a link to select different email' do - create(:email_address, user: user) - user.reload - render - - expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) - expect(rendered).to_not include(t('account.index.email_add')) - end - - it 'does not show a link to add another email' do - render - - expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) - expect(rendered).to_not include(t('account.index.email_add')) - end - end - - context 'select email to send to partner' do - it 'does not show a link to select different email' do - create(:email_address, user: user) - user.reload - render - - expect(rendered).to include(t('help_text.requested_attributes.change_email_link')) - expect(rendered).to_not include(t('account.index.email_add')) - end - - it 'does not show a link to add another email' do - render - - expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) - expect(rendered).to include(t('account.index.email_add')) - end - end - context 'the all_emails scope is requested' do let(:requested_attributes) { [:email, :all_emails] } diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb deleted file mode 100644 index a699da041ae..00000000000 --- a/spec/views/sign_up/select_email/show.html.erb_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'sign_up/select_email/show.html.erb' do - let(:email) { 'michael.motorist@email.com' } - let(:email2) { 'michael.motorist2@email.com' } - let(:user) { create(:user) } - - before do - user.email_addresses.create(email: email, confirmed_at: Time.zone.now) - user.email_addresses.create(email: email2, confirmed_at: Time.zone.now) - user.reload - @user_emails = user.email_addresses - @select_email_form = SelectEmailForm.new(user) - end - - it 'shows all of the user\'s emails' do - render - - expect(rendered).to include('michael.motorist@email.com') - expect(rendered).to include('michael.motorist2@email.com') - end -end From 67c0b87ec8f3dcb014621190144024079560bdaa Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:16:03 -0400 Subject: [PATCH 4/5] Regression specs for users with cancelled and failed in_person_enrollments (#11141) * add regressions specs for failed and cancelled in_person_enrollments * Changelog: Internal, Data Normalization, Regression Specs for Cancelled and Failed in_person_enrollments * fix test setup for cancelled and failed in_person_enrollments --- spec/views/accounts/show.html.erb_spec.rb | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index 178e15b4240..92aacb09d24 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -111,6 +111,42 @@ end end + context 'when current user has an in_person_enrollment that was failed' do + let(:vtr) { ['Pe'] } + let(:sp_name) { 'sinatra-test-app' } + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + before do + # Make the in_person_enrollment and associated profile failed + in_person_enrollment = user.in_person_enrollments.first + in_person_enrollment.update!(status: :failed, status_check_completed_at: Time.zone.now) + profile = user.profiles.first + profile.deactivate_due_to_in_person_verification_cancelled + end + + it 'renders the idv partial' do + expect(render).to render_template(partial: 'accounts/_identity_verification') + end + end + + context 'when current user has an in_person_enrollment that was cancelled' do + let(:vtr) { ['Pe'] } + let(:sp_name) { 'sinatra-test-app' } + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + before do + # Make the in_person_enrollment and associated profile cancelled + in_person_enrollment = user.in_person_enrollments.first + in_person_enrollment.update!(status: :cancelled, status_check_completed_at: Time.zone.now) + profile = user.profiles.first + profile.deactivate_due_to_in_person_verification_cancelled + end + + it 'renders the idv partial' do + expect(render).to render_template(partial: 'accounts/_identity_verification') + end + end + context 'when current user has an in_person_enrollment that expired' do let(:vtr) { ['Pe'] } let(:sp_name) { 'sinatra-test-app' } From 114b3e869f7fa2a2c42ed66343b19ddfdfbe20f7 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Thu, 29 Aug 2024 16:18:01 -0400 Subject: [PATCH 5/5] Revert "Merge pull request #11169 from 18F/stages/rc-2024-08-29-revert" This reverts commit d4c042a996e859e0c204feba966b5325add9b598, reversing changes made to 8a7595f2b158d258f4e8b369cf8bc4a3587dcd9b. --- Gemfile | 1 + Gemfile.lock | 1 + .../icon_list_item_component.html.erb | 2 +- app/controllers/application_controller.rb | 2 +- app/controllers/concerns/mfa_setup_concern.rb | 2 +- .../concerns/saml_idp_auth_concern.rb | 9 + .../authorization_controller.rb | 8 + .../sign_up/completions_controller.rb | 5 + .../sign_up/select_email_controller.rb | 54 ++++++ app/controllers/socure_webhook_controller.rb | 4 + .../users/piv_cac_recommended_controller.rb | 6 +- ..._factor_authentication_setup_controller.rb | 6 +- app/forms/openid_connect_authorize_form.rb | 4 +- app/forms/select_email_form.rb | 31 ++++ app/jobs/get_usps_proofing_results_job.rb | 13 +- app/models/email_address.rb | 22 ++- app/models/federal_email_domain.rb | 7 + app/models/profile.rb | 2 +- app/models/service_provider_identity.rb | 2 + app/models/user.rb | 4 +- app/presenters/completions_presenter.rb | 56 +++--- .../openid_connect_user_info_presenter.rb | 2 +- .../set_up_piv_cac_selection_presenter.rb | 2 +- .../two_factor_options_presenter.rb | 1 - app/services/analytics_events.rb | 8 - app/services/attribute_asserter.rb | 12 +- app/services/displayable_pii_formatter.rb | 10 +- app/services/doc_auth_router.rb | 11 +- app/services/identity_linker.rb | 4 +- .../idv/aamva_state_maintenance_window.rb | 163 ++++++++++++++++++ app/services/idv/analytics_events_enhancer.rb | 1 - app/services/proofing/aamva/proofer.rb | 11 +- app/services/proofing/state_id_result.rb | 12 +- app/views/sign_up/completions/show.html.erb | 11 ++ app/views/sign_up/select_email/show.html.erb | 50 ++++++ config/application.yml.default | 17 +- config/initializers/ab_tests.rb | 10 +- config/locales/en.yml | 12 +- config/locales/es.yml | 12 +- config/locales/fr.yml | 12 +- config/locales/zh.yml | 12 +- config/routes.rb | 2 + ...40809152808_create_federal_email_domain.rb | 9 + ...41_add_aaguid_to_webauthn_configuration.rb | 5 + db/schema.rb | 8 +- lib/ab_test.rb | 2 +- lib/identity_config.rb | 10 +- lib/idp/constants.rb | 1 + lib/tasks/federal_email_domains.rake | 17 ++ package.json | 2 +- spec/config/initializers/ab_tests_spec.rb | 20 +-- .../sign_up/select_email_controller_spec.rb | 53 ++++++ .../socure_webhook_controller_spec.rb | 13 ++ .../piv_cac_recommended_controller_spec.rb | 13 +- ...or_authentication_setup_controller_spec.rb | 42 ++++- spec/factories/federal_email_domain.rb | 4 + spec/factories/in_person_enrollments.rb | 3 + spec/features/idv/analytics_spec.rb | 3 +- .../multiple_emails/sp_sign_in_spec.rb | 106 +++++++++++- .../piv_recommended_after_sign_in_spec.rb | 132 ++++++++++++++ spec/features/users/sign_in_spec.rb | 24 --- spec/features/users/sign_up_spec.rb | 121 +++++++++++-- spec/forms/delete_user_email_form_spec.rb | 12 ++ .../openid_connect_authorize_form_spec.rb | 2 + spec/forms/select_email_form_spec.rb | 60 +++++++ .../get_usps_proofing_results_job_spec.rb | 142 ++++++++++----- spec/models/email_address_spec.rb | 69 +++++++- spec/models/profile_spec.rb | 4 +- spec/models/user_spec.rb | 37 ++++ spec/presenters/completions_presenter_spec.rb | 116 +++++++------ ...openid_connect_user_info_presenter_spec.rb | 42 +++++ ...set_up_piv_cac_selection_presenter_spec.rb | 3 +- spec/services/attribute_asserter_spec.rb | 130 ++++++++++++++ .../displayable_pii_formatter_spec.rb | 10 +- spec/services/doc_auth_router_spec.rb | 38 ---- .../aamva_state_maintenance_window_spec.rb | 68 ++++++++ spec/services/idv/proofing_components_spec.rb | 4 +- spec/services/proofing/aamva/proofer_spec.rb | 24 +++ .../mock/state_id_mock_client_spec.rb | 3 + .../services/proofing/state_id_result_spec.rb | 3 + spec/views/accounts/show.html.erb_spec.rb | 2 +- .../sign_up/completions/show.html.erb_spec.rb | 58 ++++++- .../select_email/show.html.erb_spec.rb | 22 +++ yarn.lock | 49 ++---- 84 files changed, 1743 insertions(+), 359 deletions(-) create mode 100644 app/controllers/sign_up/select_email_controller.rb create mode 100644 app/forms/select_email_form.rb create mode 100644 app/models/federal_email_domain.rb create mode 100644 app/services/idv/aamva_state_maintenance_window.rb create mode 100644 app/views/sign_up/select_email/show.html.erb create mode 100644 db/primary_migrate/20240809152808_create_federal_email_domain.rb create mode 100644 db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb create mode 100644 lib/tasks/federal_email_domains.rake create mode 100644 spec/controllers/sign_up/select_email_controller_spec.rb create mode 100644 spec/factories/federal_email_domain.rb create mode 100644 spec/features/sign_in/piv_recommended_after_sign_in_spec.rb create mode 100644 spec/forms/select_email_form_spec.rb create mode 100644 spec/services/idv/aamva_state_maintenance_window_spec.rb create mode 100644 spec/views/sign_up/select_email/show.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index 7b54722971a..19d013e3bb5 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'dotiw', '>= 4.0.1' gem 'faraday', '~> 2' gem 'faker' gem 'faraday-retry' +gem 'fugit' gem 'foundation_emails' gem 'good_job', '~> 3.0' gem 'http_accept_language' diff --git a/Gemfile.lock b/Gemfile.lock index 36ec53aae08..3173e2b63f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -788,6 +788,7 @@ DEPENDENCIES faraday (~> 2) faraday-retry foundation_emails + fugit good_job (~> 3.0) http_accept_language i18n-tasks (~> 1.0) diff --git a/app/components/icon_list_item_component.html.erb b/app/components/icon_list_item_component.html.erb index 128a16fccb8..baa4d53d8c7 100644 --- a/app/components/icon_list_item_component.html.erb +++ b/app/components/icon_list_item_component.html.erb @@ -2,5 +2,5 @@ <%= content_tag(:div, class: icon_css_class) do %> <%= render IconComponent.new(icon: icon) %> <% end %> -
<%= content %>
+
<%= content %>
<% end %> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 047cd2de76b..bf92f6468a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -261,7 +261,7 @@ def user_needs_to_reactivate_account? end def user_recommended_for_piv_cac? - current_user.piv_cac_recommended_dismissed_at.nil? && current_user.has_gov_or_mil_email? && + current_user.piv_cac_recommended_dismissed_at.nil? && current_user.has_fed_or_mil_email? && !user_already_has_piv? end diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index e999c4c1376..ac111fae564 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -82,7 +82,7 @@ def show_skip_additional_mfa_link? end def check_if_possible_piv_user - if current_user.has_gov_or_mil_email? && current_user.piv_cac_recommended_dismissed_at.nil? + if current_user.has_fed_or_mil_email? && current_user.piv_cac_recommended_dismissed_at.nil? redirect_to login_piv_cac_recommended_path end end diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 1a055c5cac2..41b01f10f04 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -140,9 +140,18 @@ def link_identity_from_session_data link_identity( ial: resolved_authn_context_int_ial, rails_session_id: session.id, + email_address_id: email_address_id, ) end + def email_address_id + return nil unless IdentityConfig.store.feature_select_email_to_share_enabled + return user_session[:selected_email_id] if user_session[:selected_email_id].present? + identity = current_user.identities.find_by(service_provider: sp_session['issuer']) + email_id = identity&.email_address_id + return email_id if email_id.is_a? Integer + end + def identity_needs_verification? resolved_authn_context_result.identity_proofing? && current_user.identity_not_verified? end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index faa27028cba..f792dd55ac6 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -84,9 +84,17 @@ def link_identity_to_service_provider current_user: current_user, ial: resolved_authn_context_int_ial, rails_session_id: session.id, + email_address_id: email_address_id, ) end + def email_address_id + return nil unless IdentityConfig.store.feature_select_email_to_share_enabled + return user_session[:selected_email_id] if user_session[:selected_email_id].present? + identity = current_user.identities.find_by(service_provider: sp_session['issuer']) + identity&.email_address_id + end + def ial_context IalContext.new( ial: resolved_authn_context_int_ial, diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 1107fa56309..007e1609c95 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -21,6 +21,10 @@ def update track_completion_event('agency-page') update_verified_attributes send_in_person_completion_survey + if user_session[:selected_email_id].nil? + user_session[:selected_email_id] = EmailContext.new(current_user). + last_sign_in_email_address.id + end if decider.go_back_to_mobile_app? sign_user_out_and_instruct_to_go_back_to_mobile_app else @@ -49,6 +53,7 @@ def completions_presenter requested_attributes: decorated_sp_session.requested_attributes.map(&:to_sym), ial2_requested: ial2_requested?, completion_context: needs_completion_screen_reason, + selected_email_id: user_session[:selected_email_id], ) end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb new file mode 100644 index 00000000000..2c6b52a2382 --- /dev/null +++ b/app/controllers/sign_up/select_email_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module SignUp + class SelectEmailController < ApplicationController + before_action :confirm_two_factor_authenticated + before_action :verify_needs_completions_screen + + def show + @sp_name = current_sp.friendly_name || sp.agency&.name + @user_emails = user_emails + @last_sign_in_email_address = last_email + @select_email_form = build_select_email_form + end + + def create + @select_email_form = build_select_email_form + + result = @select_email_form.submit(form_params) + if result.success? + user_session[:selected_email_id] = form_params[:selected_email_id] + redirect_to sign_up_completed_path + else + flash[:error] = result.first_error_message + redirect_to sign_up_select_email_path + end + end + + def user_emails + @user_emails = current_user.confirmed_email_addresses + end + + private + + def build_select_email_form + SelectEmailForm.new(current_user) + end + + def form_params + params.fetch(:select_email_form, {}).permit(:selected_email_id) + end + + def last_email + if user_session[:selected_email_id] + user_emails.find(user_session[:selected_email_id]).email + else + EmailContext.new(current_user).last_sign_in_email_address.email + end + end + + def verify_needs_completions_screen + redirect_to account_url unless needs_completion_screen_reason + end + end +end diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 69234ac992b..14bdfd494b4 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class SocureWebhookController < ApplicationController + include RenderConditionConcern + skip_before_action :verify_authenticity_token + check_or_render_not_found -> { IdentityConfig.store.socure_webhook_enabled } + def create if token_valid? render json: { message: 'Secret token is valid.' } diff --git a/app/controllers/users/piv_cac_recommended_controller.rb b/app/controllers/users/piv_cac_recommended_controller.rb index 4335917d6df..2b504239ac3 100644 --- a/app/controllers/users/piv_cac_recommended_controller.rb +++ b/app/controllers/users/piv_cac_recommended_controller.rb @@ -8,7 +8,7 @@ class PivCacRecommendedController < ApplicationController before_action :confirm_user_authenticated_for_2fa_setup before_action :apply_secure_headers_override - before_action :redirect_unless_user_email_is_gov_or_mil + before_action :redirect_unless_user_email_is_fed_or_mil def show @recommended_presenter = PivCacRecommendedPresenter.new(current_user) @@ -30,8 +30,8 @@ def skip private - def redirect_unless_user_email_is_gov_or_mil - redirect_to after_sign_in_path_for(current_user) unless current_user.has_gov_or_mil_email? + def redirect_unless_user_email_is_fed_or_mil + redirect_to after_sign_in_path_for(current_user) unless current_user.has_fed_or_mil_email? end end end diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 7804b188d15..0ef866928b7 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -16,7 +16,7 @@ def index @presenter = two_factor_options_presenter analytics.user_registration_2fa_setup_visit( enabled_mfa_methods_count:, - gov_or_mil_email: has_gov_or_mil_email?, + gov_or_mil_email: fed_or_mil_email?, ) end @@ -44,8 +44,8 @@ def two_factor_options_form private - def has_gov_or_mil_email? - current_user.confirmed_email_addresses.any?(&:gov_or_mil?) + def fed_or_mil_email? + current_user.confirmed_email_addresses.any?(&:fed_or_mil_email?) end def mfa_context diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index a70dcf8d09d..bbe816167a7 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -94,7 +94,8 @@ def service_provider def link_identity_to_service_provider( current_user:, ial:, - rails_session_id: + rails_session_id:, + email_address_id: ) identity_linker = IdentityLinker.new(current_user, service_provider) @identity = identity_linker.link_identity( @@ -106,6 +107,7 @@ def link_identity_to_service_provider( requested_aal_value: requested_aal_value, scope: scope.join(' '), code_challenge: code_challenge, + email_address_id: email_address_id, ) end diff --git a/app/forms/select_email_form.rb b/app/forms/select_email_form.rb new file mode 100644 index 00000000000..165d5e0f331 --- /dev/null +++ b/app/forms/select_email_form.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class SelectEmailForm + include ActiveModel::Model + include ActionView::Helpers::TranslationHelper + + attr_reader :user, :selected_email_id + + validate :validate_owns_selected_email + + def initialize(user) + @user = user + end + + def submit(params) + @selected_email_id = params[:selected_email_id] + + success = valid? + FormResponse.new(success:, errors:) + end + + private + + def validate_owns_selected_email + return if user.confirmed_email_addresses.exists?(id: selected_email_id) + + errors.add :email, I18n.t( + 'email_address.not_found', + ), type: :selected_email_id + end +end diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 058faadb939..ed290f1ecee 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -236,7 +236,7 @@ def handle_unsupported_id_type(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) - + enrollment.profile.deactivate_due_to_in_person_verification_cancelled # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) send_failed_email(enrollment.user, enrollment) @@ -271,7 +271,7 @@ def handle_expired_status_update(enrollment, response, response_message) status: :expired, status_check_completed_at: Time.zone.now, ) - enrollment.profile.deactivate_due_to_ipp_expiration + enrollment.profile.deactivate_due_to_in_person_verification_cancelled if fraud_result_pending?(enrollment) analytics(user: enrollment.user).idv_ipp_deactivated_for_never_visiting_post_office( @@ -325,8 +325,10 @@ def handle_fraud_review_pending(enrollment) end def handle_unexpected_response(enrollment, response_message, reason:, cancel: true) - enrollment.cancelled! if cancel - + if cancel + enrollment.cancelled! + enrollment.profile.deactivate_due_to_in_person_verification_cancelled + end analytics(user: enrollment.user). idv_in_person_usps_proofing_results_job_unexpected_response( **enrollment_analytics_attributes(enrollment, complete: cancel), @@ -352,7 +354,7 @@ def handle_failed_status(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) - + enrollment.profile.deactivate_due_to_in_person_verification_cancelled # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) if response['fraudSuspected'] @@ -442,6 +444,7 @@ def handle_unsupported_secondary_id(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) + enrollment.profile.deactivate_due_to_in_person_verification_cancelled # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) send_failed_email(enrollment.user, enrollment) diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 0f4ca204a6a..1692826343d 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -11,6 +11,7 @@ class EmailAddress < ApplicationRecord # rubocop:disable Rails/HasManyOrHasOneDependent has_one :suspended_email # rubocop:enable Rails/HasManyOrHasOneDependent + has_many :identities, class_name: 'ServiceProviderIdentity', dependent: :nullify scope :confirmed, -> { where('confirmed_at IS NOT NULL') } @@ -29,8 +30,25 @@ def confirmation_period_expired? Time.zone.now > expiration_time end - def gov_or_mil? - email.end_with?('.gov', '.mil') + def domain + Mail::Address.new(email).domain + end + + def fed_or_mil_email? + fed_email? || mil_email? + end + + def fed_email? + if IdentityConfig.store.use_fed_domain_class + return false unless domain + FederalEmailDomain.fed_domain?(domain) + else + email.end_with?('.gov') + end + end + + def mil_email? + email.end_with?('.mil') end class << self diff --git a/app/models/federal_email_domain.rb b/app/models/federal_email_domain.rb new file mode 100644 index 00000000000..1ba01ea0801 --- /dev/null +++ b/app/models/federal_email_domain.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class FederalEmailDomain < ApplicationRecord + def self.fed_domain?(domain) + exists?(name: domain) + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 0ddb3b925bd..e26ab003fa3 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -196,7 +196,7 @@ def deactivate_due_to_gpo_expiration ) end - def deactivate_due_to_ipp_expiration + def deactivate_due_to_in_person_verification_cancelled update!( active: false, deactivation_reason: :verification_cancelled, diff --git a/app/models/service_provider_identity.rb b/app/models/service_provider_identity.rb index dd17e94a0f3..5030aea024c 100644 --- a/app/models/service_provider_identity.rb +++ b/app/models/service_provider_identity.rb @@ -19,6 +19,8 @@ class ServiceProviderIdentity < ApplicationRecord # rubocop:enable Rails/InverseOf has_one :agency, through: :service_provider_record + belongs_to :email_address + scope :not_deleted, -> { where(deleted_at: nil) } CONSENT_EXPIRATION = 1.year.freeze diff --git a/app/models/user.rb b/app/models/user.rb index 0da6ccc47e2..faeaa7ddfb6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,8 +79,8 @@ def confirmed? email_addresses.where.not(confirmed_at: nil).any? end - def has_gov_or_mil_email? - confirmed_email_addresses.any?(&:gov_or_mil?) + def has_fed_or_mil_email? + confirmed_email_addresses.any?(&:fed_or_mil_email?) end def accepted_rules_of_use_still_valid? diff --git a/app/presenters/completions_presenter.rb b/app/presenters/completions_presenter.rb index e83a3bb37f3..55f8cadb77a 100644 --- a/app/presenters/completions_presenter.rb +++ b/app/presenters/completions_presenter.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true class CompletionsPresenter - attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, :completion_context + include ActionView::Helpers::TranslationHelper + include ActionView::Helpers::TagHelper + + attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, + :completion_context, :selected_email_id SORTED_IAL2_ATTRIBUTE_MAPPING = [ [[:email], :email], @@ -30,7 +34,8 @@ def initialize( decrypted_pii:, requested_attributes:, ial2_requested:, - completion_context: + completion_context:, + selected_email_id: ) @current_user = current_user @current_sp = current_sp @@ -38,6 +43,7 @@ def initialize( @requested_attributes = requested_attributes @ial2_requested = ial2_requested @completion_context = completion_context + @selected_email_id = selected_email_id end def ial2_requested? @@ -72,33 +78,24 @@ def heading end def intro - if ial2_requested? - if consent_has_expired? - I18n.t( - 'help_text.requested_attributes.ial2_consent_reminder_html', - sp: sp_name, - ) - elsif reverified_after_consent? - I18n.t( - 'help_text.requested_attributes.ial2_reverified_consent_info', - sp: sp_name, - ) - else - I18n.t( - 'help_text.requested_attributes.ial2_intro_html', - sp: sp_name, - ) - end - elsif consent_has_expired? - I18n.t( - 'help_text.requested_attributes.ial1_consent_reminder_html', - sp: sp_name, + if consent_has_expired? + safe_join( + [ + t( + 'help_text.requested_attributes.consent_reminder_html', + sp_html: content_tag(:strong, sp_name), + ), + t('help_text.requested_attributes.intro_html', sp_html: content_tag(:strong, sp_name)), + ], + ' ', ) - else - I18n.t( - 'help_text.requested_attributes.ial1_intro_html', - sp: sp_name, + elsif ial2_requested? && reverified_after_consent? + t( + 'help_text.requested_attributes.ial2_reverified_consent_info_html', + sp_html: content_tag(:strong, sp_name), ) + else + t('help_text.requested_attributes.intro_html', sp_html: content_tag(:strong, sp_name)) end end @@ -108,6 +105,10 @@ def pii end end + def multiple_emails? + current_user.confirmed_email_addresses.many? + end + private def first_time_signing_in? @@ -118,6 +119,7 @@ def displayable_pii @displayable_pii ||= DisplayablePiiFormatter.new( current_user: current_user, pii: decrypted_pii, + selected_email_id: @selected_email_id, ).format end diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index 1fd4f925e22..bc87e20231b 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -54,7 +54,7 @@ def uuid_from_sp_identity(identity) end def email_from_sp_identity - email_context.last_sign_in_email_address.email + identity.email_address&.email || email_context.last_sign_in_email_address.email end def all_emails_from_sp_identity(identity) diff --git a/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb index 79b9dc5b540..a6b599df37d 100644 --- a/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb @@ -19,7 +19,7 @@ def phishing_resistant? end def recommended? - user.confirmed_email_addresses.any?(&:gov_or_mil?) + user.confirmed_email_addresses.any?(&:fed_or_mil_email?) end def desktop_only? diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 6c97445e661..bee7ac40787 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -11,7 +11,6 @@ class TwoFactorOptionsPresenter :user_agent delegate :two_factor_enabled?, to: :mfa_policy - def initialize( user_agent:, user: nil, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index edad5db7413..d1a2b002959 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1329,14 +1329,6 @@ def idv_doc_auth_link_sent_visited(**extra) track_event('IdV: doc auth link_sent visited', **extra) end - def idv_doc_auth_randomizer_defaulted(**extra) - track_event( - 'IdV: doc_auth random vendor error', - error: 'document_capture_session_uuid_key missing', - **extra, - ) - end - def idv_doc_auth_redo_ssn_submitted(**extra) track_event('IdV: doc auth redo_ssn submitted', **extra) end diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index 125a720d3be..d2320b77b10 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -200,7 +200,10 @@ def attribute_getter_function_ascii(attr) def add_email(attrs) attrs[:email] = { - getter: ->(principal) { EmailContext.new(principal).last_sign_in_email_address.email }, + getter: ->(principal) { + last_email_from_sp(principal) || + EmailContext.new(principal).last_sign_in_email_address.email + }, name_format: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', name_id_format: Saml::XML::Namespaces::Formats::NameId::EMAIL_ADDRESS, } @@ -214,6 +217,13 @@ def add_all_emails(attrs) } end + def last_email_from_sp(principal) + return nil unless IdentityConfig.store.feature_select_email_to_share_enabled + identity = principal.active_identity_for(service_provider) + email_id = identity&.email_address_id + principal.confirmed_email_addresses.find_by(id: email_id)&.email if email_id + end + def bundle @bundle ||= ( authn_request_bundle || service_provider.metadata[:attribute_bundle] || [] diff --git a/app/services/displayable_pii_formatter.rb b/app/services/displayable_pii_formatter.rb index a9c61ad1580..1bc6399fd0b 100644 --- a/app/services/displayable_pii_formatter.rb +++ b/app/services/displayable_pii_formatter.rb @@ -8,10 +8,12 @@ class DisplayablePiiFormatter attr_reader :current_user attr_reader :pii + attr_reader :selected_email_id - def initialize(current_user:, pii:) + def initialize(current_user:, pii:, selected_email_id:) @current_user = current_user @pii = pii + @selected_email_id = selected_email_id end # @return [FormattedPii] @@ -36,7 +38,11 @@ def format private def email - EmailContext.new(current_user).last_sign_in_email_address.email + if @selected_email_id + current_user.confirmed_email_addresses.find(@selected_email_id).email + else + EmailContext.new(current_user).last_sign_in_email_address.email + end end def all_emails diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb index 650c403b95c..a4c43f1d75f 100644 --- a/app/services/doc_auth_router.rb +++ b/app/services/doc_auth_router.rb @@ -196,9 +196,14 @@ def self.client(vendor:, warn_notifier: nil) # rubocop:enable Layout/LineLength def self.doc_auth_vendor_for_bucket(bucket) - bucket == :alternate_vendor ? - IdentityConfig.store.doc_auth_vendor_randomize_alternate_vendor : - IdentityConfig.store.doc_auth_vendor + case bucket + when :socure + Idp::Constants::Vendors::SOCURE + when :lexis_nexis + Idp::Constants::Vendors::LEXIS_NEXIS + else # e.g., nil + IdentityConfig.store.doc_auth_vendor_default + end end def self.doc_auth_vendor( diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index b34f76077f7..8f69f422560 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -25,7 +25,8 @@ def link_identity( scope: nil, verified_attributes: nil, last_consented_at: nil, - clear_deleted_at: nil + clear_deleted_at: nil, + email_address_id: nil ) return unless user && service_provider.present? @@ -43,6 +44,7 @@ def link_identity( rails_session_id: rails_session_id, scope: scope, verified_attributes: combined_verified_attributes(verified_attributes), + email_address_id: email_address_id, ).tap do |hash| hash[:last_consented_at] = last_consented_at if last_consented_at hash[:deleted_at] = nil if clear_deleted_at diff --git a/app/services/idv/aamva_state_maintenance_window.rb b/app/services/idv/aamva_state_maintenance_window.rb new file mode 100644 index 00000000000..422e45d122c --- /dev/null +++ b/app/services/idv/aamva_state_maintenance_window.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module Idv + class AamvaStateMaintenanceWindow + # _All_ AAMVA maintenance windows are expressed in 'ET' (LG-14028) + TZ = 'America/New_York' + + MAINTENANCE_WINDOWS = { + 'CA' => [ + # Daily, 4:00 - 5:30 am. ET. + { cron: '0 4 * * *', duration_minutes: 90 }, + # Monday, 1:00 - 1:45 am. ET + { cron: '0 1 * * Mon', duration_minutes: 45 }, + # Monday, 1:00 - 4:30 am. ET on 1st and 3rd Monday of month. + { cron: '0 1 * * Mon#1', duration_minutes: 3.5 * 60 }, + { cron: '0 1 * * Mon#3', duration_minutes: 3.5 * 60 }, + ], + 'CT' => [ + # Daily, 4:00 am. to 6:30 am. ET. + { cron: '0 4 * * *', duration_minutes: 90 }, + # Sunday 6:00 am. to 9:30 am. ET + { cron: '0 6 * * Mon', duration_minutes: 3.5 * 60 }, + ], + 'DC' => [ + # Daily, Midnight to 6 am. ET. + { cron: '0 0 * * *', duration_minutes: 6 * 60 }, + ], + 'DE' => [ + # Daily, Midnight to 5 am. ET. + { cron: '0 0 * * *', duration_minutes: 5 * 60 }, + ], + 'FL' => [ + # Sunday 7:00 am. to 12:00 pm. ET + { cron: '0 7 * * Sun', duration_minutes: 5 * 60 }, + ], + 'IA' => [ + # "Daily system resets, normally at 4:45 am. to 5:15 am ET." + { cron: '45 4 * * *', duration_minutes: 30 }, + ], + 'IN' => [ + # Sunday morning maintenance from 6 am. to 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, + ], + 'IL' => [ + { cron: '30 2 * * *', duration_minutes: 2.5 * 60 }, # Daily, 2:30 am. to 5 am. ET. + ], + 'KY' => [ + # Daily maintenance from 2:50 am. to 6:40 am. ET + { cron: '50 2 * * *', duration_minutes: 230 }, + ], + 'MA' => [ + # Daily maintenance from 6 am. to 6:15 am. ET. + { cron: '0 6 * * *', duration_minutes: 15 }, + # Wednesday 7 am. to 7:30 am. ET. + { cron: '0 7 * * Wed', duration_minutes: 30 }, + # Saturday 10:00 pm. to Sunday 10:00 am + { cron: '0 22 * * Sat', duration_minutes: 12 * 60 }, + # First Friday of each month: 12 to 6 am. ET. + { cron: '0 0 * * Fri#1', duration_minutes: 6 * 60 }, + ], + 'MD' => [ + # Daily maintenance from 3 am. to 3:15 am. ET. + { cron: '0 3 * * *', duration_minutes: 15 }, + # Sunday maintenance may occur from 6 am. to 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, + ], + 'MI' => [ + # Daily maintenance from 9 pm. to 9:15 pm. ET. + { cron: '0 21 * * *', duration_minutes: 15 }, + ], + 'MO' => [ + # Daily maintenance from 2 am. to 4:30 am. ... + { cron: '0 2 * * *', duration_minutes: 2.5 * 60 }, + # ... from 6:30 am to 6:45 am ... + { cron: '30 6 * * *', duration_minutes: 15 }, + # ... and 8:30 am. to 8:35 am ET. + { cron: '30 8 * * *', duration_minutes: 5 }, + # Sundays from 9 am. to 10:30 am. ET... + { cron: '0 9 * * Sun', duration_minutes: 90 }, + # ...and 5 am to 5:45 am ET on 2nd Sunday of month. + { cron: '0 5 * * Sun#2', duration_minutes: 45 }, + ], + 'NC' => [ + # Daily, Midnight to 7:00 am. ET. + { cron: '0 0 * * *', duration_minutes: 7 * 60 }, + # Sundays from 5am. till Noon + { cron: '0 5 * * Sun', duration_minutes: 7 * 60 }, + ], + # NM: "Sunday mornings." (not modeling; too vague) + 'NY' => [ + # Sunday maintenance 8 pm. to 9 pm. ET. + { cron: '0 20 * * Sun', duration_minutes: 60 }, + ], + 'PA' => [ + # Sunday maintenance may occur, often between 5:30 am. & 7:00 am. ET + { cron: '30 5 * * Sun', duration_minutes: 90 }, + ], + 'SC' => [ + # Sunday maintenance from 7:00 pm. to 10:00 pm. ET. + { cron: '0 19 * * Sun', duration_minutes: 3 * 60 }, + ], + 'TX' => [ + # Downtime on weekends between 9 pm ET to 7 am ET. + { cron: '0 21 * * Sat,Sun', duration_minutes: 10 * 60 }, + ], + 'VA' => [ + # Sunday morning maintenance 3:00 am. to 5 am. ET. + { cron: '0 3 * * Sun', duration_minutes: 120 }, + # Daily maintenance from 5 am. to 5:30 am. + { cron: '0 5 * * *', duration_minutes: 30 }, + # "Might not respond for short spells, daily between 7 pm and 8:30 pm." (not modeling this) + ], + 'VT' => [ + # Daily maintenance from midnight to 5 am. ET. + { cron: '0 0 * * *', duration_minutes: 5 * 60 }, + ], + 'WA' => [ + # Maintenance from Saturday 9:45 pm. to Sunday 8:15 am. ET. + { cron: '45 21 * * Sat', duration_minutes: 10.5 * 60 }, + ], + 'WI' => [ + # Downtime on Tuesday – Saturday typically between 3 – 4 am ET. + { cron: '0 3 * * Tue-Sat', duration_minutes: 60 }, + # Downtime on Sunday from 6 – 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, + ], + 'WV' => [ + # Occasional Sunday maintenance from 6:00 am. to noon ET. + { cron: '0 6 * * Sun', duration_minutes: 6 * 60 }, + ], + 'WY' => [ + # Daily, 2 am. to 5 am. ET. + { cron: '0 2 * * *', duration_minutes: 3 * 60 }, + ], + }.freeze + + PARSED_MAINTENANCE_WINDOWS = MAINTENANCE_WINDOWS.transform_values do |windows| + Time.use_zone(TZ) do + windows.map do |window| + cron = Fugit.parse_cron(window[:cron]) + { cron: cron, duration_minutes: window[:duration_minutes] } + end + end + end.freeze + + class << self + def in_maintenance_window?(state) + Time.use_zone(TZ) do + windows_for_state(state).any? { |window| window.cover?(Time.zone.now) } + end + end + + def windows_for_state(state) + Time.use_zone(TZ) do + PARSED_MAINTENANCE_WINDOWS.fetch(state, []).map do |window| + previous = window[:cron].previous_time.to_t + (previous..(previous + window[:duration_minutes].minutes)) + end + end + end + end + end +end diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index ab470910044..11163c5f185 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -25,7 +25,6 @@ module AnalyticsEventsEnhancer idv_doc_auth_hybrid_handoff_visited idv_doc_auth_link_sent_submitted idv_doc_auth_link_sent_visited - idv_doc_auth_randomizer_defaulted idv_doc_auth_redo_ssn_submitted idv_doc_auth_ssn_submitted idv_doc_auth_ssn_visited diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index 48616af80d0..374a83572f1 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -49,7 +49,7 @@ def proof(applicant) ).send_verification_request( applicant: aamva_applicant, ) - build_result_from_response(response) + build_result_from_response(response, applicant[:state]) rescue => exception failed_result = Proofing::StateIdResult.new( success: false, errors: {}, exception: exception, vendor_name: 'aamva:state_id', @@ -61,7 +61,7 @@ def proof(applicant) private - def build_result_from_response(verification_response) + def build_result_from_response(verification_response, jurisdiction) Proofing::StateIdResult.new( success: verification_response.success?, errors: parse_verification_errors(verification_response), @@ -70,11 +70,12 @@ def build_result_from_response(verification_response) transaction_id: verification_response.transaction_locator_id, requested_attributes: requested_attributes(verification_response).index_with(1), verified_attributes: verified_attributes(verification_response), + jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?(jurisdiction), ) end def parse_verification_errors(verification_response) - errors = errors = Hash.new { |h, k| h[k] = [] } + errors = Hash.new { |h, k| h[k] = [] } return errors if verification_response.success? @@ -119,6 +120,10 @@ def send_to_new_relic(result) end NewRelic::Agent.notice_error(result.exception) end + + def jurisdiction_in_maintenance_window?(state) + Idv::AamvaStateMaintenanceWindow.in_maintenance_window?(state) + end end end end diff --git a/app/services/proofing/state_id_result.rb b/app/services/proofing/state_id_result.rb index 02b2b0416ba..a2e5f1d6c10 100644 --- a/app/services/proofing/state_id_result.rb +++ b/app/services/proofing/state_id_result.rb @@ -8,7 +8,6 @@ class StateIdResult attr_reader :errors, :exception, - :success, :vendor_name, :transaction_id, :requested_attributes, @@ -21,7 +20,8 @@ def initialize( vendor_name: nil, transaction_id: '', requested_attributes: {}, - verified_attributes: [] + verified_attributes: [], + jurisdiction_in_maintenance_window: false ) @success = success @errors = errors @@ -30,10 +30,11 @@ def initialize( @transaction_id = transaction_id @requested_attributes = requested_attributes @verified_attributes = verified_attributes + @jurisdiction_in_maintenance_window = jurisdiction_in_maintenance_window end def success? - success + !!@success end def timed_out? @@ -56,6 +57,10 @@ def mva_exception? mva_unavailable? || mva_system_error? || mva_timeout? end + def jurisdiction_in_maintenance_window? + !!@jurisdiction_in_maintenance_window + end + def to_h { success: success?, @@ -67,6 +72,7 @@ def to_h transaction_id: transaction_id, vendor_name: vendor_name, verified_attributes: verified_attributes, + jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?, } end end diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index 6dbe1572268..e019e17a33d 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -42,6 +42,17 @@ last_number: attribute_value[-1], ), ) %> + <% elsif attribute_key == :email && IdentityConfig.store.feature_select_email_to_share_enabled %> +
+ <%= attribute_value.to_s %> +

+ <% if @presenter.multiple_emails? %> + <%= link_to t('help_text.requested_attributes.change_email_link'), sign_up_select_email_path %> + <% else %> + <%= link_to t('account.index.email_add'), add_email_path %> + <% end %> +

+
<% else %> <%= attribute_value.to_s %> <% end %> diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb new file mode 100644 index 00000000000..c89ce9a7676 --- /dev/null +++ b/app/views/sign_up/select_email/show.html.erb @@ -0,0 +1,50 @@ +<% self.title = t('titles.select_email') %> + +<%= render StatusPageComponent.new(status: :info, icon: :question) do |c| %> + <% c.with_header { t('titles.select_email') } %> +

+ <%= I18n.t('help_text.select_preferred_email', sp: @sp_name, app_name: APP_NAME) %> +

+ + <%= simple_form_for('', url: sign_up_select_email_path) do |f| %> +
+
+
+
+ <% @user_emails.each do |email, index| %> +
+ <%= radio_button_tag( + 'select_email_form[selected_email_id]', + email.id, + email.email == @last_sign_in_email_address, + class: 'usa-radio__input usa-radio__input--bordered', + ) %> + <%= label_tag( + "select_email_form_selected_email_id_#{email.id}", + class: 'usa-radio__label width-full', + ) do %> + <%= email.email %> + <% end %> +
+ <% end %> +
+
+
+
+
+ <%= f.submit t('help_text.requested_attributes.change_email_link'), class: 'margin-top-4' %> + <% end %> + + <%= render ButtonComponent.new( + url: add_email_path, + outline: true, + big: true, + wide: true, + class: 'margin-top-2', + ).with_content(t('account.index.email_add')) %> + + <%= render PageFooterComponent.new do %> + <%= link_to t('forms.buttons.back'), sign_up_completed_path %> + <% end %> + +<% end %> \ No newline at end of file diff --git a/config/application.yml.default b/config/application.yml.default index 017ef81a2d7..72726ab5de9 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -97,9 +97,9 @@ doc_auth_max_submission_attempts_before_native_camera: 3 doc_auth_selfie_desktop_test_mode: false doc_auth_separate_pages_enabled: false doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]' -doc_auth_vendor_randomize: false -doc_auth_vendor_randomize_alternate_vendor: '' -doc_auth_vendor_randomize_percent: 0 +doc_auth_vendor_lexis_nexis_percent: 100 # note, LN is currently the default vendor +doc_auth_vendor_socure_percent: 0 +doc_auth_vendor_switching_enabled: false doc_capture_polling_enabled: true doc_capture_request_valid_for_minutes: 15 drop_off_report_config: '[{"emails":["ursula@example.com"],"issuers": ["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]}]' @@ -116,6 +116,7 @@ enable_usps_verification: true event_disavowal_expiration_hours: 240 feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true +feature_select_email_to_share_enabled: true geo_data_file_path: 'geo_data/GeoLite2-City.mmdb' get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 @@ -335,6 +336,7 @@ sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1 sign_in_user_id_per_ip_attempt_window_in_minutes: 720 sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200 sign_in_user_id_per_ip_max_attempts: 50 +socure_webhook_enabled: false socure_webhook_secret_key: '' socure_webhook_secret_key_queue: '[]' sp_handoff_bounce_max_seconds: 2 @@ -349,6 +351,7 @@ test_ssn_allowed_list: '' totp_code_interval: 30 unauthorized_scope_enabled: false use_dashboard_service_providers: false +use_fed_domain_class: false use_kms: false use_vot_in_sp_requests: true usps_auth_token_refresh_job_enabled: false @@ -398,6 +401,7 @@ development: dashboard_url: http://localhost:3001/api/service_providers doc_auth_selfie_desktop_test_mode: true doc_auth_vendor: 'mock' + doc_auth_vendor_default: 'mock' domain_name: localhost:3000 enable_rate_limiting: false hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c @@ -432,6 +436,7 @@ development: state_tracking_enabled: true telephony_adapter: test use_dashboard_service_providers: true + use_fed_domain_class: true usps_eipp_sponsor_id: '222222222222222' usps_ipp_sponsor_id: '111111111111111' usps_ipp_transliteration_enabled: true @@ -456,11 +461,13 @@ production: dashboard_url: https://dashboard.demo.login.gov disable_email_sending: false disable_logout_get_request: false - doc_auth_vendor: 'acuant' + doc_auth_vendor: 'lexisnexis' + doc_auth_vendor_default: 'lexisnexis' domain_name: login.gov email_registrations_per_ip_track_only_mode: true enable_test_routes: false enable_usps_verification: false + feature_select_email_to_share_enabled: false hmac_fingerprinter_key: hmac_fingerprinter_key_queue: '[]' idv_sp_required: true @@ -511,6 +518,7 @@ test: doc_auth_max_attempts: 4 doc_auth_selfie_desktop_test_mode: true doc_auth_vendor: 'mock' + doc_auth_vendor_default: 'mock' doc_capture_polling_enabled: false domain_name: www.example.com email_registrations_per_ip_limit: 3 @@ -563,6 +571,7 @@ test: telephony_adapter: test test_ssn_allowed_list: '999999999' totp_code_interval: 3 + use_fed_domain_class: true usps_eipp_sponsor_id: '222222222222222' usps_ipp_root_url: 'http://localhost:1000' usps_ipp_sponsor_id: '111111111111111' diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 38b69641eed..70fdddd95c2 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -35,13 +35,17 @@ def self.all constants.index_with { |test_name| const_get(test_name) } end + # This "test" will permanently be in place to allow a graceful transition from TrueID being the + # sole vendor to a multi-vendor configuration. DOC_AUTH_VENDOR = AbTest.new( experiment_name: 'Doc Auth Vendor', should_log: /^idv/i, + default_bucket: :lexis_nexis, buckets: { - alternate_vendor: IdentityConfig.store.doc_auth_vendor_randomize ? - IdentityConfig.store.doc_auth_vendor_randomize_percent : - 0, + socure: IdentityConfig.store.doc_auth_vendor_switching_enabled ? + IdentityConfig.store.doc_auth_vendor_socure_percent : 0, + lexis_nexis: IdentityConfig.store.doc_auth_vendor_switching_enabled ? + IdentityConfig.store.doc_auth_vendor_lexis_nexis_percent : 0, }.compact, ) do |service_provider:, session:, user:, user_session:, **| document_capture_session_uuid_discriminator(service_provider:, session:, user:, user_session:) diff --git a/config/locales/en.yml b/config/locales/en.yml index f1a3b37a92f..9dca9c10a6c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -692,6 +692,7 @@ doc_auth.tips.review_issues_id_text1: Did you use a dark background? doc_auth.tips.review_issues_id_text2: Did you take the photo on a flat surface? doc_auth.tips.review_issues_id_text3: Is the flash on your camera off? doc_auth.tips.review_issues_id_text4: Are all details sharp and clearly visible? +email_address.not_found: 'Email not found' email_addresses.add.duplicate: This email address is already registered to your account. email_addresses.add.limit: You’ve added the maximum number of email addresses. email_addresses.delete.bullet1: You won’t be able to sign in to %{app_name} (or any of the government applications linked to your account) using this email address @@ -958,18 +959,18 @@ headings.webauthn_setup.new: Insert your security key help_text.requested_attributes.address: Address help_text.requested_attributes.all_emails: Email addresses on your account help_text.requested_attributes.birthdate: Date of birth +help_text.requested_attributes.change_email_link: Change +help_text.requested_attributes.consent_reminder_html: You must consent each year to share your information with %{sp_html}. help_text.requested_attributes.email: Email address help_text.requested_attributes.full_name: Full name -help_text.requested_attributes.ial1_consent_reminder_html: You must consent each year to share your information with %{sp}. We’ll share your information with %{sp} to connect your account. -help_text.requested_attributes.ial1_intro_html: We’ll share your information with %{sp} to connect your account. -help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} needs to know who you are to connect to your account. You must consent each year to share your verified information with %{sp}. We’ll share this information:' -help_text.requested_attributes.ial2_intro_html: '%{sp} needs to know who you are to connect your account. We’ll share this information with %{sp}:' -help_text.requested_attributes.ial2_reverified_consent_info: 'Because you verified your identity again, we need your permission to share this information with %{sp}:' +help_text.requested_attributes.ial2_reverified_consent_info_html: 'Because you verified your identity again, we need your permission to share this information with %{sp_html}:' +help_text.requested_attributes.intro_html: 'We’ll share this information with %{sp_html}:' help_text.requested_attributes.phone: Phone number help_text.requested_attributes.social_security_number: Social Security number help_text.requested_attributes.verified_at: Updated on help_text.requested_attributes.x509_issuer: PIV/CAC Issuer help_text.requested_attributes.x509_subject: PIV/CAC Identity +help_text.select_preferred_email: You may change which email you share with %{sp} since you have multiple emails associated with your %{app_name} account. i18n.language: Language i18n.locale.en: English i18n.locale.es: Español @@ -1606,6 +1607,7 @@ titles.reactivate_account: Reactivate your account titles.registrations.new: Create your account titles.revoke_consent: Revoke Consent titles.rules_of_use: Rules of Use +titles.select_email: Select your preferred email titles.sign_up.completion_consent_expired_ial1: It’s been a year since you gave us consent to share your information titles.sign_up.completion_consent_expired_ial2: It’s been a year since you gave us consent to share your verified identity titles.sign_up.completion_first_sign_in: Continue to %{sp} diff --git a/config/locales/es.yml b/config/locales/es.yml index 2d5bcae878a..de9a55b97fb 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -703,6 +703,7 @@ doc_auth.tips.review_issues_id_text1: '¿Usó un fondo de color oscuro?' doc_auth.tips.review_issues_id_text2: '¿Tomó la foto en una superficie plana?' doc_auth.tips.review_issues_id_text3: '¿Está apagado el flash de su cámara?' doc_auth.tips.review_issues_id_text4: '¿Todos los detalles se ven con precisión y claridad?' +email_address.not_found: El correo electrónico no encontrado email_addresses.add.duplicate: Esta dirección de correo electrónico ya está registrada en su cuenta. email_addresses.add.limit: Agregó el número máximo de direcciones de correo electrónico. email_addresses.delete.bullet1: Si usa esta dirección de correo electrónico, no podrá iniciar sesión en %{app_name} (ni en ninguna de las aplicaciones gubernamentales vinculadas a su cuenta). @@ -969,18 +970,18 @@ headings.webauthn_setup.new: Inserte su clave de seguridad help_text.requested_attributes.address: Dirección help_text.requested_attributes.all_emails: Direcciones de correo electrónico en su cuenta help_text.requested_attributes.birthdate: Fecha de nacimiento +help_text.requested_attributes.change_email_link: Cambiar +help_text.requested_attributes.consent_reminder_html: Debe dar su consentimiento cada año para divulgar su información a %{sp_html}. help_text.requested_attributes.email: Dirección de correo electrónico help_text.requested_attributes.full_name: Nombre completo -help_text.requested_attributes.ial1_consent_reminder_html: Debe dar su consentimiento cada año para divulgar su información a %{sp}. Divulgaremos su información a %{sp} para conectar su cuenta. -help_text.requested_attributes.ial1_intro_html: Divulgaremos su información a %{sp} para conectar su cuenta. -help_text.requested_attributes.ial2_consent_reminder_html: 'Para conectar su cuenta, %{sp} necesita saber quién es usted. Debe dar su consentimiento cada año para divulgar su información verificada a %{sp}. Divulgaremos esta información:' -help_text.requested_attributes.ial2_intro_html: 'Para conectar su cuenta, %{sp} necesita saber quién es usted. Divulgaremos esta información a %{sp}:' -help_text.requested_attributes.ial2_reverified_consent_info: 'Como volvió a verificar su identidad, necesitamos su permiso para divulgar esta información a %{sp}:' +help_text.requested_attributes.ial2_reverified_consent_info_html: 'Como volvió a verificar su identidad, necesitamos su permiso para divulgar esta información a %{sp_html}:' +help_text.requested_attributes.intro_html: 'Divulgaremos esta información a %{sp_html}:' help_text.requested_attributes.phone: Número de teléfono help_text.requested_attributes.social_security_number: Número de Seguro Social help_text.requested_attributes.verified_at: Actualizado en help_text.requested_attributes.x509_issuer: Emisor de la tarjeta PIV o CAC help_text.requested_attributes.x509_subject: Identidad de la tarjeta PIV o CAC +help_text.select_preferred_email: Puede cambiar el correo electrónico que comparte con %{sp} ya que tiene varios correos electrónicos asociados a su cuenta de %{app_name}. i18n.language: Idioma i18n.locale.en: English i18n.locale.es: Español @@ -1618,6 +1619,7 @@ titles.reactivate_account: Reactive su cuenta titles.registrations.new: Cree su cuenta titles.revoke_consent: Revocar consentimiento titles.rules_of_use: Reglas de uso +titles.select_email: Seleccione el correo electrónico que prefiera titles.sign_up.completion_consent_expired_ial1: Hace un año que nos dio su consentimiento para divulgar su información titles.sign_up.completion_consent_expired_ial2: Hace un año que nos dio su consentimiento para divulgar su identidad verificada titles.sign_up.completion_first_sign_in: Continuar con %{sp} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a1489bc0f48..2cf64287c9b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -692,6 +692,7 @@ doc_auth.tips.review_issues_id_text1: Avez-vous utilisé un arrière-plan de cou doc_auth.tips.review_issues_id_text2: Avez-vous pris la photo sur une surface plane ? doc_auth.tips.review_issues_id_text3: Le flash de votre appareil photo est-il éteint ? doc_auth.tips.review_issues_id_text4: Tous les détails sont-ils nets et clairement visibles ? +email_address.not_found: Email non trouvé email_addresses.add.duplicate: Cette adresse e-mail est déjà enregistrée sur votre compte. email_addresses.add.limit: Vous avez ajouté le nombre maximum d’adresses e-mail. email_addresses.delete.bullet1: Vous ne pourrez pas vous connecter à %{app_name} (ni à aucune des applications de l’administration associées à votre compte) à l’aide de cette adresse e-mail @@ -958,18 +959,18 @@ headings.webauthn_setup.new: Insérer votre clé de sécurité help_text.requested_attributes.address: Adresse help_text.requested_attributes.all_emails: Adresses e-mail sur votre compte help_text.requested_attributes.birthdate: Date de naissance +help_text.requested_attributes.change_email_link: Modifier +help_text.requested_attributes.consent_reminder_html: Vous devez consentir chaque année au partage de vos informations avec %{sp_html}. help_text.requested_attributes.email: Adresse e-mail help_text.requested_attributes.full_name: Nom complet -help_text.requested_attributes.ial1_consent_reminder_html: Vous devez consentir chaque année au partage de vos informations avec %{sp}. Nous partagerons vos informations avec %{sp} pour connecter votre compte. -help_text.requested_attributes.ial1_intro_html: Nous partagerons vos informations avec %{sp} pour connecter votre compte. -help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} a besoin de savoir qui vous êtes pour se connecter à votre compte. Vous devez consentir chaque année à partager vos informations vérifiées avec %{sp}. Nous partagerons ces informations :' -help_text.requested_attributes.ial2_intro_html: '%{sp} a besoin de savoir qui vous êtes pour connecter votre compte. Nous partagerons ces informations avec %{sp} :' -help_text.requested_attributes.ial2_reverified_consent_info: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp} :' +help_text.requested_attributes.ial2_reverified_consent_info_html: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp_html} :' +help_text.requested_attributes.intro_html: 'Nous partagerons ces informations avec %{sp_html}:' help_text.requested_attributes.phone: Numéro de téléphone help_text.requested_attributes.social_security_number: Numéro de sécurité sociale help_text.requested_attributes.verified_at: Mis à jour le help_text.requested_attributes.x509_issuer: Émetteur PIV/CAC help_text.requested_attributes.x509_subject: Identité PIV/CAC +help_text.select_preferred_email: Vous pouvez modifier l’adresse e-mail que vous partagez avec %{sp} car vous possédez plusieurs adresses e-mail associées à votre compte %{app_name}. i18n.language: Langue i18n.locale.en: English i18n.locale.es: Español @@ -1606,6 +1607,7 @@ titles.reactivate_account: Réactiver votre compte titles.registrations.new: Créer votre compte titles.revoke_consent: Révoquer le consentement titles.rules_of_use: Règles d’utilisation +titles.select_email: Sélectionner l’adresse e-mail de votre choix titles.sign_up.completion_consent_expired_ial1: Cela fait un an que vous nous avez donné votre consentement pour partager vos informations titles.sign_up.completion_consent_expired_ial2: Cela fait un an que vous nous avez donné votre consentement pour partager votre identité vérifiée titles.sign_up.completion_first_sign_in: Continuer vers %{sp} diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 316555349c5..d2c92c4f34b 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -703,6 +703,7 @@ doc_auth.tips.review_issues_id_text1: 你是否使用了暗色背景? doc_auth.tips.review_issues_id_text2: 是否在平坦平面上拍的照? doc_auth.tips.review_issues_id_text3: 你相机的闪光灯是否关闭? doc_auth.tips.review_issues_id_text4: 是否所有细节都清晰可见? +email_address.not_found: 未找到电子邮件 email_addresses.add.duplicate: 该电邮地址已注册到你的账户。 email_addresses.add.limit: 你添加的电邮地址数目已达最多。 email_addresses.delete.bullet1: 使用该电邮地址你无法登录进入 %{app_name} (或任何其他与你账户关联的政府应用程序)。 @@ -971,18 +972,18 @@ headings.webauthn_setup.new: 插入您的安全密钥 help_text.requested_attributes.address: 地址 help_text.requested_attributes.all_emails: 你账户上的电邮地址 help_text.requested_attributes.birthdate: 生日 +help_text.requested_attributes.change_email_link: 更改 +help_text.requested_attributes.consent_reminder_html: 你每年都必须授权同意与 %{sp_html} 分享信息。 help_text.requested_attributes.email: 电邮地址 help_text.requested_attributes.full_name: 姓名 -help_text.requested_attributes.ial1_consent_reminder_html: 你每年都必须授权同意与 %{sp} 分享信息。我们将与 %{sp} 分享你的信息来连接你账户。 -help_text.requested_attributes.ial1_intro_html: 我们将与 %{sp} 分享你的信息来连接你账户。 -help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} 需要知道你是谁才能连接你的账户。你每年都必须授权同意与 %{sp} 分享已验证过的你的信息。我们会分享这些信息:' -help_text.requested_attributes.ial2_intro_html: '%{sp} 需要知道你是谁才能连接你的账户。我们会与 %{sp} 分享这些信息:' -help_text.requested_attributes.ial2_reverified_consent_info: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp} 分享该信息。' +help_text.requested_attributes.ial2_reverified_consent_info_html: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp_html} 分享该信息。' +help_text.requested_attributes.intro_html: 我们会与 %{sp_html} 分享这些信息: help_text.requested_attributes.phone: 电话号码 help_text.requested_attributes.social_security_number: 社会保障号码 help_text.requested_attributes.verified_at: 更新是在 help_text.requested_attributes.x509_issuer: PIV/CAC 发放方 help_text.requested_attributes.x509_subject: PIV/CAC 身份 +help_text.select_preferred_email: 因为你有多个电邮与 %{app_name} 账户相关,你可以更改与我们 %{sp} 构分享哪个。 i18n.language: 语言 i18n.locale.en: English i18n.locale.es: Español @@ -1619,6 +1620,7 @@ titles.reactivate_account: 重新激活你账户 titles.registrations.new: 设立账户 titles.revoke_consent: 撤销同意 titles.rules_of_use: 使用规则 +titles.select_email: 选择你比较愿意分享的电邮 titles.sign_up.completion_consent_expired_ial1: 从你上次授权我们分享你的信息已经一年了。 titles.sign_up.completion_consent_expired_ial2: 从你上次授权我们分享你验证过的身份已经一年了。 titles.sign_up.completion_first_sign_in: 继续到 %{sp} diff --git a/config/routes.rb b/config/routes.rb index 30863b3e145..eb35a338ee7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -304,6 +304,8 @@ get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register get '/sign_up/enter_password' => 'sign_up/passwords#new' + get '/sign_up/select_email' => 'sign_up/select_email#show' + post '/sign_up/select_email' => 'sign_up/select_email#create' get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed post '/sign_up/completed' => 'sign_up/completions#update' diff --git a/db/primary_migrate/20240809152808_create_federal_email_domain.rb b/db/primary_migrate/20240809152808_create_federal_email_domain.rb new file mode 100644 index 00000000000..ef255ae82cc --- /dev/null +++ b/db/primary_migrate/20240809152808_create_federal_email_domain.rb @@ -0,0 +1,9 @@ +class CreateFederalEmailDomain < ActiveRecord::Migration[7.1] + def change + create_table :federal_email_domains do |t| + t.citext :name, null: false + end + + add_index :federal_email_domains, :name, unique: true + end +end diff --git a/db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb b/db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb new file mode 100644 index 00000000000..bed1e8ca51c --- /dev/null +++ b/db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb @@ -0,0 +1,5 @@ +class AddAaguidToWebauthnConfiguration < ActiveRecord::Migration[7.1] + def change + add_column :webauthn_configurations, :aaguid, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index ef682f62526..385018287e3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_08_22_122355) do +ActiveRecord::Schema[7.1].define(version: 2024_08_28_182041) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" @@ -226,6 +226,11 @@ t.index ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at" end + create_table "federal_email_domains", force: :cascade do |t| + t.citext "name", null: false + t.index ["name"], name: "index_federal_email_domains_on_name", unique: true + end + create_table "fraud_review_requests", force: :cascade do |t| t.integer "user_id" t.string "uuid" @@ -651,6 +656,7 @@ t.boolean "platform_authenticator" t.string "transports", array: true t.jsonb "authenticator_data_flags" + t.string "aaguid" t.index ["user_id"], name: "index_webauthn_configurations_on_user_id" end diff --git a/lib/ab_test.rb b/lib/ab_test.rb index ef090aabe43..840cd1cc828 100644 --- a/lib/ab_test.rb +++ b/lib/ab_test.rb @@ -5,7 +5,7 @@ class AbTest MAX_SHA = (16 ** 64) - 1 - # @param [Proc,RegExp,string,Boolean,nil] should_log Controls whether bucket data for this + # @param [Proc,Regexp,string,Boolean,nil] should_log Controls whether bucket data for this # A/B test is logged with specific # events. # @yieldparam [ActionDispatch::Request] request diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 51d032a7b1e..246d4210f73 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -117,9 +117,10 @@ def self.store config.add(:doc_auth_separate_pages_enabled, type: :boolean) config.add(:doc_auth_supported_country_codes, type: :json) config.add(:doc_auth_vendor, type: :string) - config.add(:doc_auth_vendor_randomize, type: :boolean) - config.add(:doc_auth_vendor_randomize_alternate_vendor, type: :string) - config.add(:doc_auth_vendor_randomize_percent, type: :integer) + config.add(:doc_auth_vendor_default, type: :string) + config.add(:doc_auth_vendor_lexis_nexis_percent, type: :integer) + config.add(:doc_auth_vendor_socure_percent, type: :integer) + config.add(:doc_auth_vendor_switching_enabled, type: :boolean) config.add(:doc_capture_polling_enabled, type: :boolean) config.add(:doc_capture_request_valid_for_minutes, type: :integer) config.add(:drop_off_report_config, type: :json) @@ -137,6 +138,7 @@ def self.store config.add(:event_disavowal_expiration_hours, type: :integer) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) + config.add(:feature_select_email_to_share_enabled, type: :boolean) config.add(:geo_data_file_path, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) @@ -383,6 +385,7 @@ def self.store config.add(:sign_in_user_id_per_ip_max_attempts, type: :integer) config.add(:sign_in_recaptcha_score_threshold, type: :float) config.add(:skip_encryption_allowed_list, type: :json) + config.add(:socure_webhook_enabled, type: :boolean) config.add(:socure_webhook_secret_key, type: :string) config.add(:socure_webhook_secret_key_queue, type: :json) config.add(:sp_handoff_bounce_max_seconds, type: :integer) @@ -404,6 +407,7 @@ def self.store config.add(:usps_auth_token_refresh_job_enabled, type: :boolean) config.add(:usps_confirmation_max_days, type: :integer) config.add(:usps_eipp_sponsor_id, type: :string) + config.add(:use_fed_domain_class, type: :boolean) config.add(:usps_ipp_client_id, type: :string) config.add(:usps_ipp_password, type: :string) config.add(:usps_ipp_request_timeout, type: :integer) diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 5a3b32bce42..d7ba925e42d 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -12,6 +12,7 @@ module Constants module Vendors ACUANT = 'acuant' LEXIS_NEXIS = 'lexis_nexis' + SOCURE = 'socure' MOCK = 'mock' USPS = 'usps' AAMVA = 'aamva' diff --git a/lib/tasks/federal_email_domains.rake b/lib/tasks/federal_email_domains.rake new file mode 100644 index 00000000000..95ac234d520 --- /dev/null +++ b/lib/tasks/federal_email_domains.rake @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'faraday' +require 'csv' + +DOT_GOV_DOWNLOAD_PATH = 'https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-federal.csv' +namespace :federal_email_domains do + task load_to_db: :environment do |_task, _args| + response = Faraday.get(DOT_GOV_DOWNLOAD_PATH) + + csv = CSV.parse(response.body, col_sep: ',', headers: true) + csv.each do |row| + FederalEmailDomain.find_or_create_by(name: row['Domain name']) + end + end +end +# rake "federal_email_domains:load_to_db" diff --git a/package.json b/package.json index 65f2d0b3321..521c35d6c8d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "source-map-loader": "^4.0.0", - "webpack": "^5.91.0", + "webpack": "^5.94.0", "webpack-assets-manifest": "^5.2.1", "webpack-cli": "^5.1.4" }, diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index b6313e51bd6..f858344346d 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -3,13 +3,7 @@ RSpec.describe AbTests do describe '#all' do it 'returns all registered A/B tests' do - expect(AbTests.all).to match( - { - ACUANT_SDK: an_instance_of(AbTest), - DOC_AUTH_VENDOR: an_instance_of(AbTest), - - }, - ) + expect(AbTests.all.values).to all(be_kind_of(AbTest)) end end @@ -119,20 +113,20 @@ let(:enable_ab_test) do -> { - allow(IdentityConfig.store).to receive(:doc_auth_vendor). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default). and_return('vendor_a') - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). and_return(true) - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_alternate_vendor). - and_return('vendor_b') - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_percent). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_socure_percent). and_return(50) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_lexis_nexis_percent). + and_return(30) } end let(:disable_ab_test) do -> { - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). and_return(false) } end diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb new file mode 100644 index 00000000000..898c402091d --- /dev/null +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe SignUp::SelectEmailController do + describe 'before_actions' do + it 'requires the user be logged in and authenticated' do + expect(subject).to have_actions( + :before, + :confirm_two_factor_authenticated, + ) + end + + it 'requires the user be in the completions flow' do + expect(subject).to have_actions( + :before, + :verify_needs_completions_screen, + ) + end + end + + describe '#create' do + let(:email) { 'michael.motorist@email.com' } + let(:email2) { 'michael.motorist2@email.com' } + let(:email3) { 'david.motorist@email.com' } + let(:user) { create(:user) } + + before do + user.email_addresses = [] + create(:email_address, user:, email: email) + create(:email_address, user:, email: email2) + end + + it 'updates selected email address' do + post :create, params: { selected_email_id: email2 } + + expect(user.email_addresses.last.email). + to include('michael.motorist2@email.com') + end + + context 'with a corrupted email selected_email_id form' do + render_views + it 'rejects email not belonging to the user' do + stub_sign_in(user) + allow(controller).to receive(:needs_completion_screen_reason).and_return(true) + post :create, params: { selected_email_id: email3 } + + expect(user.email_addresses.last.email). + to include('michael.motorist2@email.com') + + expect(response).to redirect_to(sign_up_select_email_path) + end + end + end +end diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb index 04a06d36d03..23d88f59fb7 100644 --- a/spec/controllers/socure_webhook_controller_spec.rb +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -6,12 +6,15 @@ describe 'POST /api/webhooks/socure/event' do let(:socure_secret_key) { 'this-is-a-secret' } let(:socure_secret_key_queue) { ['this-is-an-old-secret', 'this-is-an-older-secret'] } + let(:socure_webhook_enabled) { true } before do allow(IdentityConfig.store).to receive(:socure_webhook_secret_key). and_return(socure_secret_key) allow(IdentityConfig.store).to receive(:socure_webhook_secret_key_queue). and_return(socure_secret_key_queue) + allow(IdentityConfig.store).to receive(:socure_webhook_enabled). + and_return(socure_webhook_enabled) end it 'returns OK with a correct secret key' do @@ -40,5 +43,15 @@ expect(response).to have_http_status(:unauthorized) end + + context 'when socure webhook disabled' do + let(:socure_webhook_enabled) { false } + it 'the webhook route does not exist' do + request.headers['Authorization'] = socure_secret_key + post :create + + expect(response).to be_not_found + end + end end end diff --git a/spec/controllers/users/piv_cac_recommended_controller_spec.rb b/spec/controllers/users/piv_cac_recommended_controller_spec.rb index a9bd996aa9f..69d179b4506 100644 --- a/spec/controllers/users/piv_cac_recommended_controller_spec.rb +++ b/spec/controllers/users/piv_cac_recommended_controller_spec.rb @@ -2,7 +2,8 @@ RSpec.describe Users::PivCacRecommendedController do describe 'New user' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_sign_in_before_2fa(user) stub_analytics @@ -28,10 +29,10 @@ end describe 'Sign in flow' do - let(:user) { create(:user, :with_phone, { email: 'example@example.gov' }) } + let(:user) { create(:user, :with_phone, { email: 'example@gsa.gov' }) } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_analytics - stub_sign_in(user) user.reload end @@ -49,7 +50,8 @@ end context '#confirm' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_sign_in_before_2fa(user) stub_analytics @@ -77,7 +79,8 @@ end context '#skip' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_sign_in_before_2fa(user) stub_analytics 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 e22cfc60ffd..5672ea37eb6 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -19,10 +19,16 @@ ) end - context 'with user having gov or mil email' do + context 'with user having gov or mil email and use_fed_domain_class set to false' do let(:user) do create(:user, email: 'example@example.gov', piv_cac_recommended_dismissed_at: Time.zone.now) end + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } + + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + context 'having already visited the PIV interstitial page' do it 'tracks the visit in analytics' do get :index @@ -48,6 +54,40 @@ end end + context 'with user having gov or mil email and use_fed_domain_class set to true' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } + let(:user) do + create(:user, email: 'example@gsa.gov', piv_cac_recommended_dismissed_at: Time.zone.now) + end + context 'having already visited the PIV interstitial page' do + it 'tracks the visit in analytics' do + get :index + + expect(@analytics).to have_logged_event( + 'User Registration: 2FA Setup visited', + enabled_mfa_methods_count: 0, + gov_or_mil_email: true, + ) + end + end + + context 'directed to page without having visited PIV interstitial page' do + let(:user) do + create(:user, email: 'example@gsa.gov') + end + + it 'redirects user to piv_recommended_path' do + get :index + + expect(response).to redirect_to(login_piv_cac_recommended_url) + end + end + end + context 'when signed out' do let(:user) { nil } diff --git a/spec/factories/federal_email_domain.rb b/spec/factories/federal_email_domain.rb new file mode 100644 index 00000000000..6903d7eb11a --- /dev/null +++ b/spec/factories/federal_email_domain.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :federal_email_domain do + end +end diff --git a/spec/factories/in_person_enrollments.rb b/spec/factories/in_person_enrollments.rb index 70238b182c2..bfb36296ea4 100644 --- a/spec/factories/in_person_enrollments.rb +++ b/spec/factories/in_person_enrollments.rb @@ -16,6 +16,9 @@ enrollment_established_at { Time.zone.now } status { :pending } status_updated_at { Time.zone.now } + profile do + association(:profile, :in_person_verification_pending, user: user) + end end trait :expired do diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index b321848299e..2e722d7f654 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -57,7 +57,8 @@ verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', - state_id_number: '#############' } + state_id_number: '#############', + jurisdiction_in_maintenance_window: false } end let(:resolution_block) do diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb index dbae13eb376..be6d75b55b6 100644 --- a/spec/features/multiple_emails/sp_sign_in_spec.rb +++ b/spec/features/multiple_emails/sp_sign_in_spec.rb @@ -17,15 +17,62 @@ fill_in_code_with_last_phone_otp click_submit_default click_agree_and_continue if current_path == sign_up_completed_path - decoded_id_token = fetch_oidc_id_token_info - expect(decoded_id_token[:email]).to eq(email) + expect(decoded_id_token[:email]).to eq(emails.first) expect(decoded_id_token[:all_emails]).to be_nil Capybara.reset_session! end end + scenario 'signing in with OIDC and selecting an alternative email address at first sign in' do + user = create(:user, :fully_registered, :with_multiple_emails) + emails = user.reload.email_addresses.map(&:email) + + visit_idp_from_oidc_sp(scope: 'openid email') + signin(emails.first, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_link(t('help_text.requested_attributes.change_email_link')) + + choose emails.second + + click_button(t('help_text.requested_attributes.change_email_link')) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + decoded_id_token = fetch_oidc_id_token_info + expect(decoded_id_token[:email]).to eq(emails.second) + end + + scenario 'signing in with OIDC after deleting email linked to identity' do + user = create(:user, :fully_registered) + email1 = create(:email_address, user:, email: 'email1@example.com') + email2 = create(:email_address, user:, email: 'email2@example.com') + + # Link identity with email + visit_idp_from_oidc_sp(scope: 'openid email') + signin(email1.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_link(t('help_text.requested_attributes.change_email_link')) + choose email2.email + click_button(t('help_text.requested_attributes.change_email_link')) + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + # Delete email from account + visit manage_email_confirm_delete_url(id: email2.id) + click_button t('forms.email.buttons.delete') + + # Sign in again to partner application + visit_idp_from_oidc_sp(scope: 'openid email') + + decoded_id_token = fetch_oidc_id_token_info + expect(decoded_id_token[:email]).to eq(email1.email) + end + scenario 'signing in with SAML sends the email address used to sign in' do user = create(:user, :fully_registered, :with_multiple_emails) emails = user.reload.email_addresses.map(&:email) @@ -42,12 +89,63 @@ xmldoc = SamlResponseDoc.new('feature', 'response_assertion') email_from_saml_response = xmldoc.attribute_value_for('email') - - expect(email_from_saml_response).to eq(email) + expect(email_from_saml_response).to eq(emails.first) Capybara.reset_session! end end + + scenario 'signing in with SAML and selecting an alternative email address at first sign in' do + user = create(:user, :fully_registered, :with_multiple_emails) + emails = user.reload.email_addresses.map(&:email) + + visit authn_request + signin(emails.first, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + + click_link(t('help_text.requested_attributes.change_email_link')) + choose emails.second + click_button(t('help_text.requested_attributes.change_email_link')) + + expect(current_path).to eq(sign_up_completed_path) + + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + email_from_saml_response = xmldoc.attribute_value_for('email') + expect(email_from_saml_response).to eq(emails.second) + end + + scenario 'signing in with SAML after deleting email linked to identity' do + user = create(:user, :fully_registered) + email1 = create(:email_address, user:, email: 'email1@example.com') + email2 = create(:email_address, user:, email: 'email2@example.com') + + # Link identity with email + visit authn_request + signin(email1.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + click_link(t('help_text.requested_attributes.change_email_link')) + choose email2.email + click_button(t('help_text.requested_attributes.change_email_link')) + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + # Delete email from account + visit manage_email_confirm_delete_url(id: email2.id) + click_button t('forms.email.buttons.delete') + + # Sign in again to partner application + visit authn_request + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + email_from_saml_response = xmldoc.attribute_value_for('email') + expect(email_from_saml_response).to eq(email1.email) + end end context 'with the all_emails scope' do diff --git a/spec/features/sign_in/piv_recommended_after_sign_in_spec.rb b/spec/features/sign_in/piv_recommended_after_sign_in_spec.rb new file mode 100644 index 00000000000..2906a36a34e --- /dev/null +++ b/spec/features/sign_in/piv_recommended_after_sign_in_spec.rb @@ -0,0 +1,132 @@ +require 'rails_helper' + +RSpec.feature 'Piv recommended after Sign in' do + context 'use_fed_domain_class set to true' do + let!(:federal_email_domain) { create(:federal_email_domain, name: 'gsa.gov') } + + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + scenario 'User with valid fed email directed to recommend page and get to setup piv' do + user = create(:user, :with_phone, { email: 'example@gsa.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with mil email directed to recommended PIV page and goes to add piv page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with fed email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@gsa.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with mil email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with invalid .gov email directed to account page' do + user = create(:user, :with_phone, { email: 'example@bad.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(account_path) + end + end + + context 'use_fed_domain_class set to false' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + scenario 'User with .gov email directed to recommend page and get to setup piv' do + user = create(:user, :with_phone, { email: 'example@good.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with .mil email directed to recommended PIV page and goes to add piv page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with fed email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@example.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with mil email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with invalid no .gov or .mil email directed to account page' do + user = create(:user, :with_phone, { email: 'example@bad.com' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(account_path) + end + end +end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index b05147aaea5..215c19fe083 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -102,30 +102,6 @@ expect(oidc_redirect_url).to start_with service_provider.redirect_uris.first end - scenario 'User with gov/mil email directed to recommended PIV page' do - user = create(:user, :with_phone, { email: 'example@example.gov' }) - - visit new_user_session_path - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - expect(page).to have_current_path(login_piv_cac_recommended_path) - click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) - expect(page).to have_current_path(setup_piv_cac_path) - end - - scenario 'User with gov/mil email and skips recommendation page' do - user = create(:user, :with_phone, { email: 'example@example.gov' }) - - visit new_user_session_path - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - expect(page).to have_current_path(login_piv_cac_recommended_path) - click_button(t('two_factor_authentication.piv_cac_upsell.skip')) - expect(page).to have_current_path(account_path) - end - scenario 'user attempts sign in with piv/cac with no account then creates account' do visit_idp_from_sp_with_ial1(:oidc) click_on t('account.login.piv_cac') diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index 6d1d2f0fa95..b71f4ce0a78 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -511,30 +511,117 @@ def clipboard_text end end - describe 'mil or gov email account' do - before do - confirm_email('test@test.gov') - submit_form_with_valid_password - end - it 'should land user on piv cac suggestion page' do - expect(current_path).to eq login_piv_cac_recommended_path - end + describe 'User Directed to Piv Cac recommended' do + context 'set config use_fed_domain_class to false' do + let(:email) { 'test@test.gov' } + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end - context 'user can skip piv cac prompt' do - it 'should skip piv cac prompt and land on mfa screen' do + it 'should land user on piv cac suggestion page' do + confirm_email(email) + submit_form_with_valid_password expect(current_path).to eq login_piv_cac_recommended_path - click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + end - expect(current_path).to eq authentication_methods_setup_path + context 'user can skip piv cac prompt' do + it 'should skip piv cac prompt and land on mfa screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + + expect(current_path).to eq authentication_methods_setup_path + end + end + + context 'user who selects to add piv is directed to piv screen' do + it 'should be directed straight to piv add screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + + expect(current_path).to eq setup_piv_cac_path + end end end - context 'user who selects to add piv is directed to piv screen' do - it 'should be directed straight to piv add screen' do - expect(current_path).to eq login_piv_cac_recommended_path - click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + context 'set config use_fed_domain_class to true' do + let!(:federal_email_domain) { create(:federal_email_domain, name: 'gsa.gov') } + let(:email) { 'test@gsa.gov' } + + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + context 'valid fed email' do + it 'should land user on piv cac suggestion page when fed government' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + end + + context 'user can skip piv cac prompt' do + it 'should skip piv cac prompt and land on mfa screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + + expect(current_path).to eq authentication_methods_setup_path + end + end + + context 'user who selects to add piv is directed to piv screen' do + it 'should be directed straight to piv add screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + + expect(current_path).to eq setup_piv_cac_path + end + end + end + + context 'any mil email' do + let(:email) { 'test@example.mil' } + it 'should land user on piv cac suggestion page when fed government' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + end + + context 'user can skip piv cac prompt' do + it 'should skip piv cac prompt and land on mfa screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + + expect(current_path).to eq authentication_methods_setup_path + end + end + + context 'user who selects to add piv is directed to piv screen' do + it 'should be directed straight to piv add screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + + expect(current_path).to eq setup_piv_cac_path + end + end + end - expect(current_path).to eq setup_piv_cac_path + context 'invalid fed email' do + let(:email) { 'test@example.gov' } + it 'should land user on piv cac suggestion page when fed government' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq authentication_methods_setup_path + end end end end diff --git a/spec/forms/delete_user_email_form_spec.rb b/spec/forms/delete_user_email_form_spec.rb index 8bc089fa4b6..68e9fbd88b6 100644 --- a/spec/forms/delete_user_email_form_spec.rb +++ b/spec/forms/delete_user_email_form_spec.rb @@ -60,6 +60,18 @@ submit end + + it 'removes associated identity email address id' do + user.identities << ServiceProviderIdentity.create( + service_provider: 'http://localhost:3000', + last_authenticated_at: Time.zone.now, + ) + user.identities.last.email_address_id = email_address.id + + submit + + expect(user.identities.last.email_address_id).to be(nil) + end end context 'with a email of a different user' do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 568c2553535..580983f27d7 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -661,6 +661,7 @@ current_user: user, ial: 1, rails_session_id: rails_session_id, + email_address_id: 4, ) identity = user.identities.where(service_provider: client_id).first @@ -684,6 +685,7 @@ current_user: user, ial: 1, rails_session_id: rails_session_id, + email_address_id: 4, ) end diff --git a/spec/forms/select_email_form_spec.rb b/spec/forms/select_email_form_spec.rb new file mode 100644 index 00000000000..23a17bdbba5 --- /dev/null +++ b/spec/forms/select_email_form_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +RSpec.describe SelectEmailForm do + let(:user) { create(:user, :fully_registered, :with_multiple_emails) } + describe '#submit' do + it 'returns the email successfully' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: user.email_addresses.last.id) + + expect(response.success?).to eq(true) + end + + it 'returns an error when submitting an invalid email' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: nil) + + expect(response.success?).to eq(false) + end + + context 'with an unconfirmed email address added' do + before do + create( + :email_address, + email: 'michael.business@business.com', + user: user, + confirmed_at: nil, + confirmation_sent_at: 1.month.ago, + ) + end + + it 'returns an error' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: user.email_addresses.last.id) + + expect(response.success?).to eq(false) + end + end + + context 'with another user\'s email' do + let(:user2) { create(:user, :fully_registered, :with_multiple_emails) } + before do + create( + :email_address, + email: 'michael.business@business.com', + user: user2, + confirmed_at: nil, + confirmation_sent_at: 1.month.ago, + ) + @email2 = user2.email_addresses.last.id + end + + it 'returns an error' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: @email2) + + expect(response.success?).to eq(false) + end + end + end +end diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 77433303c03..454651054c6 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -259,12 +259,6 @@ before do enrollment_records = InPersonEnrollment.where(id: pending_enrollments.map(&:id)) - # Below sets in_person_verification_pending_at - # on the profile associated with each pending enrollment - enrollment_records.each do |enrollment| - profile = enrollment.profile - profile.update(in_person_verification_pending_at: enrollment.created_at) - end allow(InPersonEnrollment).to receive(:needs_usps_status_check). and_return(enrollment_records) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) @@ -824,6 +818,16 @@ request_passed_proofing_unsupported_id_results_response, ) + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil + job.perform Time.zone.now + pending_enrollment.reload + + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end + it 'logs a message about the unsupported ID' do expected_wait_until = nil freeze_time do @@ -884,10 +888,11 @@ end it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil job.perform(Time.zone.now) pending_enrollment.reload - expect(pending_enrollment.profile).not_to be_active + expect(pending_enrollment.profile.active).to be false expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') end @@ -989,6 +994,17 @@ ), ) end + + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason). + to eq('verification_cancelled') + end end context 'when a unique id is invalid' do @@ -1348,57 +1364,84 @@ ), ) end + end - context 'when the enrollment has failed' do - before do - stub_request_failed_proofing_results - end + context 'when the enrollment has failed' do + before do + stub_request_failed_proofing_results + end + + it 'sends proofing failed email on response with failed status' do + user = pending_enrollment.user - it 'sends proofing failed email on response with failed status' do - user = pending_enrollment.user - - freeze_time do - expect do - job.perform(Time.zone.now) - end.to have_enqueued_mail(UserMailer, :in_person_failed).with( - params: { user: user, email_address: user.email_addresses.first }, - args: [{ enrollment: pending_enrollment }], - ) - expect(job_analytics).to have_logged_event( - 'GetUspsProofingResultsJob: Success or failure email initiated', - hash_including( - email_type: 'Failed', - job_name: 'GetUspsProofingResultsJob', - ), - ) - end + freeze_time do + expect do + job.perform(Time.zone.now) + end.to have_enqueued_mail(UserMailer, :in_person_failed).with( + params: { user: user, email_address: user.email_addresses.first }, + args: [{ enrollment: pending_enrollment }], + ) + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Success or failure email initiated', + hash_including( + email_type: 'Failed', + job_name: 'GetUspsProofingResultsJob', + ), + ) end end - end - it 'deactivates and sets fraud related fields of an expired enrollment' do - stub_request_expired_id_ipp_proofing_results + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil - job.perform(Time.zone.now) + job.perform(Time.zone.now) + pending_enrollment.reload + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason). + to eq('verification_cancelled') + end - profile = pending_enrollment.reload.profile - expect(profile).not_to be_active - expect(profile.fraud_review_pending_at).to be_nil - expect(profile.fraud_rejection_at).not_to be_nil - expect(job_analytics).to have_logged_event( - :idv_ipp_deactivated_for_never_visiting_post_office, - ) + it 'deactivates and sets fraud related fields of an expired enrollment' do + stub_request_expired_id_ipp_proofing_results + + job.perform(Time.zone.now) + + profile = pending_enrollment.reload.profile + expect(profile).not_to be_active + expect(profile.fraud_review_pending_at).to be_nil + expect(profile.fraud_rejection_at).not_to be_nil + expect(job_analytics).to have_logged_event( + :idv_ipp_deactivated_for_never_visiting_post_office, + ) + end end end end end describe 'Proofed with secondary id' do - let(:pending_enrollment) do - create( - :in_person_enrollment, :pending - ) + let!(:pending_enrollments) do + ['BALTIMORE', 'FRIENDSHIP', 'WASHINGTON', 'ARLINGTON', 'DEANWOOD'].map do |name| + create( + :in_person_enrollment, + :pending, + :with_notification_phone_configuration, + issuer: 'http://localhost:3000', + selected_location_details: { name: name }, + sponsor_id: usps_ipp_sponsor_id, + ) + end end + let(:pending_enrollment) { pending_enrollments.first } + + before do + enrollment_records = InPersonEnrollment.where(id: pending_enrollments.map(&:id)) + allow(InPersonEnrollment).to receive(:needs_usps_status_check). + and_return(enrollment_records) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + end + before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end @@ -1417,6 +1460,16 @@ request_passed_proofing_secondary_id_type_results_response, ) + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end + it 'logs a message about enrollment with secondary ID' do allow(IdentityConfig.store).to receive( :in_person_send_proofing_notifications_enabled, @@ -1428,6 +1481,7 @@ end.to have_enqueued_job(InPerson::SendProofingNotificationJob). with(pending_enrollment.id).at(1.hour.from_now).on_queue(:intentionally_delayed) end + expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(pending_enrollment.profile.active).to eq(false) expect(job_analytics).to have_logged_event( diff --git a/spec/models/email_address_spec.rb b/spec/models/email_address_spec.rb index eefbe9832e3..50adfa6256f 100644 --- a/spec/models/email_address_spec.rb +++ b/spec/models/email_address_spec.rb @@ -89,25 +89,80 @@ end end - describe '#gov_or_mil?' do - subject(:result) { email_address.gov_or_mil? } + describe '#fed_or_mil_email?' do + subject(:result) { email_address.fed_or_mil_email? } - context 'with an email domain ending in anything other than .gov or .mil' do - let(:email) { 'example@example.com' } + context 'with an email domain that is a fed email' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + let(:email) { 'example@example.gov' } + + it { expect(result).to eq(true) } + end + + context 'with an email that is a mil email' do + let(:email) { 'example@example.mil' } + + it { expect(result).to eq(true) } + end + + context 'with an email that is not a mil or fed email' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + let(:email) { 'example@bad.gov' } it { expect(result).to eq(false) } end - context 'with an email domain ending in .gov' do + context 'with a non fed email while use_fed_domain_class set to true' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + let(:email) { 'example@good.gov' } + + it { expect(result).to eq(false) } + end + end + + describe '#mil_email?' do + subject(:result) { email_address.mil_email? } + + context 'with an email domain not a mil email' do let(:email) { 'example@example.gov' } - it { expect(result).to eq(true) } + it { expect(result).to eq(false) } end - context 'with an email domain ending in .mil' do + context 'with an email domain ending in a mil domain email' do let(:email) { 'example@example.mil' } it { expect(result).to eq(true) } end end + + describe '#fed_email?' do + subject(:result) { email_address.fed_email? } + let!(:federal_email_domain) { create(:federal_email_domain, name: 'gsa.gov') } + + context 'use_fed_domain_class set to true' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + context 'with an email domain not a fed email' do + let(:email) { 'example@bad.gov' } + + it { expect(result).to eq(false) } + end + + context 'with an email domain ending in a fed domain email' do + let(:email) { 'example@gsa.gov' } + + it { expect(result).to eq(true) } + end + end + end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index b90c1bd4dbb..0f2058980aa 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -1001,10 +1001,10 @@ end end - describe '#deactivate_due_to_ipp_expiration' do + describe '#deactivate_due_to_in_person_verification_cancelled' do let(:profile) { create(:profile, :in_person_verification_pending) } it 'updates the profile' do - profile.deactivate_due_to_ipp_expiration + profile.deactivate_due_to_in_person_verification_cancelled expect(profile.active).to be false expect(profile.deactivation_reason).to eq('verification_cancelled') diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5958a803ed2..046e855586b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1693,4 +1693,41 @@ def it_should_not_send_survey expect(user.second_last_signed_in_at).to eq(event2.reload.created_at) end end + + describe '#has_fed_or_mil_email?' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + + context 'with a valid fed email in domain file' do + let(:user) { create(:user, email: 'example@example.gov') } + it 'should return true' do + expect(user.has_fed_or_mil_email?).to eq(true) + end + end + + context 'with use_fed_domain_class set to false and random .gov email' do + let(:user) { create(:user, email: 'example@example.gov') } + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + it 'should return true' do + expect(user.has_fed_or_mil_email?).to eq(true) + end + end + + context 'with a valid mil email' do + let(:user) { create(:user, email: 'example@example.mil') } + it 'should return true' do + expect(user.has_fed_or_mil_email?).to eq(true) + end + end + + context 'with an invalid fed or mil email' do + let(:user) { create(:user, email: 'example@example.com') } + it 'should return false' do + expect(user.has_fed_or_mil_email?).to eq(false) + end + end + end end diff --git a/spec/presenters/completions_presenter_spec.rb b/spec/presenters/completions_presenter_spec.rb index 102087eaa70..132dd2fe5e8 100644 --- a/spec/presenters/completions_presenter_spec.rb +++ b/spec/presenters/completions_presenter_spec.rb @@ -1,6 +1,9 @@ require 'rails_helper' RSpec.describe CompletionsPresenter do + include ActionView::Helpers::OutputSafetyHelper + include ActionView::Helpers::TagHelper + let(:identities) do [ build( @@ -12,6 +15,7 @@ end let(:current_user) { create(:user, :fully_registered, identities: identities) } let(:current_sp) { create(:service_provider, friendly_name: 'Friendly service provider') } + let(:selected_email_id) { current_user.email_addresses.first.id } let(:decrypted_pii) do Pii::Attributes.new( first_name: 'Testy', @@ -38,12 +42,13 @@ subject(:presenter) do described_class.new( - current_user: current_user, - current_sp: current_sp, - decrypted_pii: decrypted_pii, - requested_attributes: requested_attributes, - ial2_requested: ial2_requested, - completion_context: completion_context, + current_user:, + current_sp:, + decrypted_pii:, + requested_attributes:, + ial2_requested:, + completion_context:, + selected_email_id:, ) end @@ -147,43 +152,49 @@ end describe '#intro' do - describe 'ial1' do - context 'consent has expired since the last sign in' do - let(:identities) do - [ - build( - :service_provider_identity, - service_provider: current_sp.issuer, - last_consented_at: 2.years.ago, - ), - ] - end - let(:completion_context) { :consent_expired } + it 'renders the standard intro message' do + expect(presenter.intro).to eq( + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + ) + end - it 'renders the expired IAL1 consent intro message' do - expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial1_consent_reminder_html', - sp: current_sp.friendly_name, - ), - ) - end + context 'consent has expired since the last sign in' do + let(:identities) do + [ + build( + :service_provider_identity, + service_provider: current_sp.issuer, + last_consented_at: 2.years.ago, + ), + ] end + let(:completion_context) { :consent_expired } - context 'when consent has not expired' do - it 'renders the standard intro message' do - expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial1_intro_html', - sp: current_sp.friendly_name, - ), - ) - end + it 'renders the expired consent intro message' do + expect(presenter.intro).to eq( + safe_join( + [ + t( + 'help_text.requested_attributes.consent_reminder_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + ], + ' ', + ), + ) end end describe 'ial2' do let(:ial2_requested) { true } + context 'consent has expired since the last sign in' do let(:identities) do [ @@ -196,11 +207,20 @@ end let(:completion_context) { :consent_expired } - it 'renders the expired IAL2 consent intro message' do + it 'renders the expired consent intro message' do expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial2_consent_reminder_html', - sp: current_sp.friendly_name, + safe_join( + [ + t( + 'help_text.requested_attributes.consent_reminder_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + ], + ' ', ), ) end @@ -217,22 +237,12 @@ ] end let(:completion_context) { :reverified_after_consent } - it 'renders the reverified IAL2 consent intro message' do - expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial2_reverified_consent_info', - sp: current_sp.friendly_name, - ), - ) - end - end - context 'when consent has not expired' do - it 'renders the standard intro message' do + it 'renders the reverified IAL2 consent intro message' do expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial2_intro_html', - sp: current_sp.friendly_name, + t( + 'help_text.requested_attributes.ial2_reverified_consent_info_html', + sp_html: content_tag(:strong, current_sp.friendly_name), ), ) end diff --git a/spec/presenters/openid_connect_user_info_presenter_spec.rb b/spec/presenters/openid_connect_user_info_presenter_spec.rb index e22176e0329..8aa2bfecce4 100644 --- a/spec/presenters/openid_connect_user_info_presenter_spec.rb +++ b/spec/presenters/openid_connect_user_info_presenter_spec.rb @@ -368,5 +368,47 @@ end end end + + context 'with a deleted email' do + let(:identity) do + build( + :service_provider_identity, + rails_session_id: rails_session_id, + user: create(:user, :fully_registered, :with_multiple_emails), + scope: scope, + ) + end + + before do + identity.email_address_id = identity.user.email_addresses.first.id + identity.user.email_addresses.first.delete + end + + it 'defers to user alternate email' do + expect(identity.user.reload.email_addresses.first.id). + to_not eq(identity.email_address_id) + expect(identity.user.reload.email_addresses.count).to be 1 + expect(user_info[:email]).to eq(identity.user.email_addresses.last.email) + end + end + + context 'with nil email id' do + let(:identity) do + build( + :service_provider_identity, + rails_session_id: rails_session_id, + user: create(:user, :fully_registered), + scope: scope, + ) + end + + before do + identity.email_address_id = nil + end + + it 'adds the signed in email id to the identity' do + expect(user_info[:email]).to eq(identity.user.email_addresses.last.email) + end + end end end diff --git a/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb index 3a46e5e2010..8499e9c53fb 100644 --- a/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb @@ -2,6 +2,7 @@ RSpec.describe TwoFactorAuthentication::SetUpPivCacSelectionPresenter do let(:user) { create(:user) } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } subject(:presenter) { described_class.new(user:) } describe '#type' do @@ -42,7 +43,7 @@ end context 'with a confirmed email address ending in .gov or .mil' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } it { expect(recommended).to eq(true) } end diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index 5ad52b0a9c0..f8e656f3e75 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -717,6 +717,136 @@ it_behaves_like 'unverified user' end + + context 'with a deleted email' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + create(:email_address, user:, email: 'email@example.com') + + ident = user.identities.last + ident.email_address_id = user.email_addresses.first.id + ident.save + subject.build + + user.email_addresses.first.delete + + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq 'email@example.com' + end + end + + context 'with a nil email id' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + + ident = user.identities.last + ident.email_address_id = nil + ident.save + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq user.email_addresses.last.email + end + end + + context 'select email to send to partner feature is disabled' do + before do + allow(IdentityConfig.store).to receive( + :feature_select_email_to_share_enabled, + ).and_return(false) + end + + context 'with a deleted email' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + create(:email_address, user:, email: 'email@example.com') + + ident = user.identities.last + ident.email_address_id = user.email_addresses.first.id + ident.save + subject.build + + user.email_addresses.first.delete + + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq 'email@example.com' + end + end + + context 'with a nil email id' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + + ident = user.identities.last + ident.email_address_id = nil + ident.save + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq user.email_addresses.last.email + end + end + end end describe 'aal attributes handling' do diff --git a/spec/services/displayable_pii_formatter_spec.rb b/spec/services/displayable_pii_formatter_spec.rb index b62885eaeb9..e2f6aa407bf 100644 --- a/spec/services/displayable_pii_formatter_spec.rb +++ b/spec/services/displayable_pii_formatter_spec.rb @@ -48,6 +48,8 @@ ) end + let(:selected_email_id) { current_user.email_addresses.first.id } + let(:pii) do { first_name: first_name, @@ -63,7 +65,13 @@ } end - subject(:formatter) { described_class.new(current_user: current_user, pii: pii) } + subject(:formatter) do + described_class.new( + current_user:, + pii:, + selected_email_id:, + ) + end describe '#format' do context 'ial1' do diff --git a/spec/services/doc_auth_router_spec.rb b/spec/services/doc_auth_router_spec.rb index 76ecdf41e40..6e4cc22a49f 100644 --- a/spec/services/doc_auth_router_spec.rb +++ b/spec/services/doc_auth_router_spec.rb @@ -19,44 +19,6 @@ end end - describe '.doc_auth_vendor' do - def reload_ab_test_initializer! - # undefine the AB tests instances so we can re-initialize them with different config values - AbTests.constants.each do |const_name| - AbTests.class_eval { remove_const(const_name) } - end - load Rails.root.join('config', 'initializers', 'ab_tests.rb').to_s - end - - let(:doc_auth_vendor) { 'test1' } - let(:doc_auth_vendor_randomize_alternate_vendor) { 'test2' } - let(:analytics) { FakeAnalytics.new } - let(:doc_auth_vendor_randomize_percent) { 57 } - let(:doc_auth_vendor_randomize) { true } - - before do - allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(doc_auth_vendor) - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_alternate_vendor). - and_return(doc_auth_vendor_randomize_alternate_vendor) - - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_percent). - and_return(doc_auth_vendor_randomize_percent) - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). - and_return(doc_auth_vendor_randomize) - - reload_ab_test_initializer! - end - - after do - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_percent). - and_call_original - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). - and_call_original - - reload_ab_test_initializer! - end - end - describe DocAuthRouter::DocAuthErrorTranslatorProxy do subject(:proxy) do DocAuthRouter::DocAuthErrorTranslatorProxy.new(DocAuth::Mock::DocAuthMockClient.new) diff --git a/spec/services/idv/aamva_state_maintenance_window_spec.rb b/spec/services/idv/aamva_state_maintenance_window_spec.rb new file mode 100644 index 00000000000..19560ba16d5 --- /dev/null +++ b/spec/services/idv/aamva_state_maintenance_window_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe Idv::AamvaStateMaintenanceWindow do + let(:tz) { 'America/New_York' } + let(:eastern_time) { ActiveSupport::TimeZone[tz] } + + before do + travel_to eastern_time.parse('2024-06-02T00:00:00') + end + + describe '#in_maintenance_window?' do + let(:state) { 'DC' } + + subject { described_class.in_maintenance_window?(state) } + + context 'for a state with a defined outage window' do + it 'is true during the maintenance window' do + travel_to(eastern_time.parse('June 2, 2024 at 1am')) do + expect(subject).to eq(true) + end + end + + it 'is false outside of the maintenance window' do + travel_to(eastern_time.parse('June 2, 2024 at 8am')) do + expect(subject).to eq(false) + end + end + end + + context 'for a state without a defined outage window' do + let(:state) { 'LG' } + + it 'returns false without an exception' do + expect(subject).to eq(false) + end + end + end + + describe '.windows_for_state' do + subject { described_class.windows_for_state(state) } + + context 'for a state with no entries' do + let(:state) { 'LG' } + + it 'returns an empty array for a state with no entries' do + expect(subject).to eq([]) + end + end + + context 'for a state with multiple overlapping windows' do + let(:state) { 'CA' } + let(:expected_windows) do + [ + eastern_time.parse('2024-06-01 04:00:00')..eastern_time.parse('2024-06-01 05:30:00'), + eastern_time.parse('2024-05-27 01:00:00')..eastern_time.parse('2024-05-27 01:45:00'), + eastern_time.parse('2024-05-06 01:00:00')..eastern_time.parse('2024-05-06 04:30:00'), + eastern_time.parse('2024-05-20 01:00:00')..eastern_time.parse('2024-05-20 04:30:00'), + ] + end + + it 'returns all of them as ranges' do + Time.use_zone(tz) do + expect(subject).to eq(expected_windows) + end + end + end + end +end diff --git a/spec/services/idv/proofing_components_spec.rb b/spec/services/idv/proofing_components_spec.rb index d78fb48b1fc..97e78cc5ab6 100644 --- a/spec/services/idv/proofing_components_spec.rb +++ b/spec/services/idv/proofing_components_spec.rb @@ -32,7 +32,7 @@ let(:pii_from_doc) { Idp::Constants::MOCK_IDV_APPLICANT } before do - allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return('test_vendor') + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return('test_vendor') idv_session.mark_verify_info_step_complete! idv_session.address_verification_mechanism = 'gpo' allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?). @@ -78,7 +78,7 @@ context 'doc auth' do before do - allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return('test_vendor') + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return('test_vendor') end context 'before doc auth complete' do it 'returns nil' do diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index 6afb19420a8..1de70664a09 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -293,5 +293,29 @@ end end end + + context 'when the DMV is in a defined maintenance window' do + before do + expect(Idv::AamvaStateMaintenanceWindow).to receive(:in_maintenance_window?). + and_return(true) + end + + it 'sets jurisdiction_in_maintenance_window to true' do + result = subject.proof(state_id_data) + expect(result.jurisdiction_in_maintenance_window?).to eq(true) + end + end + + context 'when the DMV is not in a defined maintenance window' do + before do + expect(Idv::AamvaStateMaintenanceWindow).to receive(:in_maintenance_window?). + and_return(false) + end + + it 'sets jurisdiction_in_maintenance_window to false' do + result = subject.proof(state_id_data) + expect(result.jurisdiction_in_maintenance_window?).to eq(false) + end + end end end diff --git a/spec/services/proofing/mock/state_id_mock_client_spec.rb b/spec/services/proofing/mock/state_id_mock_client_spec.rb index a342cff43c9..6615ac2ec5b 100644 --- a/spec/services/proofing/mock/state_id_mock_client_spec.rb +++ b/spec/services/proofing/mock/state_id_mock_client_spec.rb @@ -24,6 +24,7 @@ transaction_id: transaction_id, vendor_name: 'StateIdMock', verified_attributes: [], + jurisdiction_in_maintenance_window: false, ) end end @@ -49,6 +50,7 @@ transaction_id: transaction_id, vendor_name: 'StateIdMock', verified_attributes: [], + jurisdiction_in_maintenance_window: false, ) end end @@ -72,6 +74,7 @@ transaction_id: transaction_id, vendor_name: 'StateIdMock', verified_attributes: [], + jurisdiction_in_maintenance_window: false, ) end end diff --git a/spec/services/proofing/state_id_result_spec.rb b/spec/services/proofing/state_id_result_spec.rb index eb40e013c3f..d40e44d6568 100644 --- a/spec/services/proofing/state_id_result_spec.rb +++ b/spec/services/proofing/state_id_result_spec.rb @@ -8,6 +8,7 @@ let(:transaction_id) { 'ABCD1234' } let(:requested_attributes) { { dob: 1, first_name: 1 } } let(:verified_attributes) { [:dob, :first_name] } + let(:jurisdiction_in_maintenance_window) { false } subject do described_class.new( @@ -18,6 +19,7 @@ transaction_id:, requested_attributes:, verified_attributes:, + jurisdiction_in_maintenance_window:, ) end @@ -34,6 +36,7 @@ transaction_id: 'ABCD1234', requested_attributes: { dob: 1, first_name: 1 }, verified_attributes: [:dob, :first_name], + jurisdiction_in_maintenance_window: false, }, ) end diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index d24efb61757..178e15b4240 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -121,7 +121,7 @@ in_person_enrollment = user.in_person_enrollments.first in_person_enrollment.update!(status: :expired, status_check_completed_at: Time.zone.now) profile = user.profiles.first - profile.deactivate_due_to_ipp_expiration + profile.deactivate_due_to_in_person_verification_cancelled end it 'renders the idv partial' do diff --git a/spec/views/sign_up/completions/show.html.erb_spec.rb b/spec/views/sign_up/completions/show.html.erb_spec.rb index 41938c7ff20..352e3e293e1 100644 --- a/spec/views/sign_up/completions/show.html.erb_spec.rb +++ b/spec/views/sign_up/completions/show.html.erb_spec.rb @@ -3,6 +3,7 @@ RSpec.describe 'sign_up/completions/show.html.erb' do let(:user) { create(:user, :fully_registered) } let(:service_provider) { create(:service_provider) } + let(:selected_email_id) { user.email_addresses.first.id } let(:decrypted_pii) { {} } let(:requested_attributes) { [:email] } let(:ial2_requested) { false } @@ -22,10 +23,11 @@ CompletionsPresenter.new( current_user: user, current_sp: service_provider, - decrypted_pii: decrypted_pii, - requested_attributes: requested_attributes, - ial2_requested: ial2_requested, - completion_context: completion_context, + decrypted_pii:, + requested_attributes:, + ial2_requested:, + completion_context:, + selected_email_id:, ) end @@ -43,9 +45,9 @@ expect(text).to_not include(service_provider.agency.name) expect(text).to include( view_context.strip_tags( - I18n.t( - 'help_text.requested_attributes.ial1_intro_html', - sp: service_provider.friendly_name, + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, service_provider.friendly_name), ), ), ) @@ -59,6 +61,48 @@ ) end + context 'select email to send to partner and select email feature is disabled' do + before do + allow(IdentityConfig.store).to receive( + :feature_select_email_to_share_enabled, + ).and_return(false) + end + + it 'does not show a link to select different email' do + create(:email_address, user: user) + user.reload + render + + expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to_not include(t('account.index.email_add')) + end + + it 'does not show a link to add another email' do + render + + expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to_not include(t('account.index.email_add')) + end + end + + context 'select email to send to partner' do + it 'does not show a link to select different email' do + create(:email_address, user: user) + user.reload + render + + expect(rendered).to include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to_not include(t('account.index.email_add')) + end + + it 'does not show a link to add another email' do + render + + expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to include(t('account.index.email_add')) + end + end + context 'the all_emails scope is requested' do let(:requested_attributes) { [:email, :all_emails] } diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb new file mode 100644 index 00000000000..a699da041ae --- /dev/null +++ b/spec/views/sign_up/select_email/show.html.erb_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe 'sign_up/select_email/show.html.erb' do + let(:email) { 'michael.motorist@email.com' } + let(:email2) { 'michael.motorist2@email.com' } + let(:user) { create(:user) } + + before do + user.email_addresses.create(email: email, confirmed_at: Time.zone.now) + user.email_addresses.create(email: email2, confirmed_at: Time.zone.now) + user.reload + @user_emails = user.email_addresses + @select_email_form = SelectEmailForm.new(user) + end + + it 'shows all of the user\'s emails' do + render + + expect(rendered).to include('michael.motorist@email.com') + expect(rendered).to include('michael.motorist2@email.com') + end +end diff --git a/yarn.lock b/yarn.lock index 1bacc1cdecc..1a803270cc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1475,23 +1475,7 @@ "@types/chai" "*" "@types/chai-as-promised" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.4.5" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.5.tgz#acdfb7dd36b91cc5d812d7c093811a8f3d9b31e4" - integrity sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.5": +"@types/estree@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -1566,7 +1550,7 @@ dependencies: "@types/sizzle" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2069,10 +2053,10 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -3165,10 +3149,10 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -enhanced-resolve@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -7100,21 +7084,20 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.91.0: - version "5.91.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9" - integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw== +webpack@^5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== dependencies: - "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.12.1" "@webassemblyjs/wasm-edit" "^1.12.1" "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" + acorn-import-attributes "^1.9.5" browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.16.0" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0"