diff --git a/.github/workflows/create-deploy-pr.yml b/.github/workflows/create-deploy-pr.yml deleted file mode 100644 index 7027e598bb5..00000000000 --- a/.github/workflows/create-deploy-pr.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Create deploy PR -on: - workflow_dispatch: - inputs: - deploy_type: - description: 'Type of deploy' - required: true - type: choice - options: - - Normal - - Patch - source: - description: 'Source branch/SHA (If blank, the current SHA running on staging will be used)' - required: false - type: string -permissions: - pull-requests: write - contents: write -jobs: - create-pr: - name: Create PR - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - PATCH: ${{ inputs.deploy_type == 'Patch' && 1 || 0 }} - SOURCE: ${{ inputs.source }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Get all commits - - uses: ruby/setup-ruby@v1 - - run: scripts/create-deploy-pr diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index 9f9bd2fd323..00000000000 --- a/.github/workflows/create-release.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Create release -run-name: "Create release based on ${{ github.event.pull_request.title }}" -on: - pull_request: - types: - - closed - branches: - - 'stages/prod' -jobs: - create-release: - name: Create release after PR merge - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - steps: - - uses: actions/checkout@v4 - - run: scripts/create-release ${{ github.event.pull_request.number }} \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index d13d559d42c..20acfbd4e2d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,7 +442,7 @@ GEM pg (1.5.4) pg_query (4.2.3) google-protobuf (>= 3.22.3) - phonelib (0.8.6) + phonelib (0.8.7) pkcs11 (0.3.4) premailer (1.21.0) addressable diff --git a/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb b/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb new file mode 100644 index 00000000000..f108c15b62a --- /dev/null +++ b/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb @@ -0,0 +1,56 @@ +module Api + module Internal + module TwoFactorAuthentication + class AuthAppController < ApplicationController + include CsrfTokenConcern + include ReauthenticationRequiredConcern + + before_action :render_unauthorized, unless: :recently_authenticated_2fa? + + after_action :add_csrf_token_header_to_response + + respond_to :json + + def update + result = ::TwoFactorAuthentication::AuthAppUpdateForm.new( + user: current_user, + configuration_id: params[:id], + ).submit(name: params[:name]) + + analytics.auth_app_update_name_submitted(**result.to_h) + + if result.success? + render json: { success: true } + else + render json: { success: false, error: result.first_error_message }, status: :bad_request + end + end + + def destroy + result = ::TwoFactorAuthentication::AuthAppDeleteForm.new( + user: current_user, + configuration_id: params[:id], + ).submit + + analytics.auth_app_delete_submitted(**result.to_h) + + if result.success? + create_user_event(:authenticator_disabled) + revoke_remember_device(current_user) + event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user) + PushNotification::HttpPush.deliver(event) + render json: { success: true } + else + render json: { success: false, error: result.first_error_message }, status: :bad_request + end + end + + private + + def render_unauthorized + render json: { error: 'Unauthorized' }, status: :unauthorized + end + end + end + end +end diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb index f1b97b207a8..67b1f10dc98 100644 --- a/app/controllers/idv/in_person/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/usps_locations_controller.rb @@ -7,7 +7,6 @@ class UspsLocationsController < ApplicationController include RenderConditionConcern include UspsInPersonProofing include EffectiveUser - include UspsInPersonProofing check_or_render_not_found -> { InPersonConfig.enabled? } diff --git a/app/controllers/idv/welcome_controller.rb b/app/controllers/idv/welcome_controller.rb index c16ea600c45..77e84bbb04d 100644 --- a/app/controllers/idv/welcome_controller.rb +++ b/app/controllers/idv/welcome_controller.rb @@ -13,7 +13,7 @@ def show call('welcome', :view, true) @sp_name = decorated_sp_session.sp_name || APP_NAME - @title = t('doc_auth.headings.getting_started', sp_name: @sp_name) + @title = t('doc_auth.headings.welcome', sp_name: @sp_name) end def update diff --git a/app/controllers/test/oidc_test_controller.rb b/app/controllers/test/oidc_test_controller.rb new file mode 100644 index 00000000000..a77b4849e8e --- /dev/null +++ b/app/controllers/test/oidc_test_controller.rb @@ -0,0 +1,147 @@ +require './spec/support/oidc_auth_helper' +module Test + class OidcTestController < ApplicationController + include OidcAuthHelper + + BIOMETRIC_REQUIRED = 'biometric-comparison-required' + + def initialize + @client_id = 'urn:gov:gsa:openidconnect:sp:test' + super + end + + def index + # default to require + @start_url_selfie = "#{test_oidc_auth_request_url}?ial=biometric-comparison-required" + @start_url_ial2 = "#{test_oidc_auth_request_url}?ial=2" + @start_url_ial1 = "#{test_oidc_auth_request_url}?ial=1" + update_service_provider + end + + def auth_request + ial = prepare_step_up_flow(ial: params[:ial]) + + idp_url = authorization_url( + ial: ial, + aal: params[:aal], + ) + + Rails.logger.info("Redirecting to #{idp_url}") + + redirect_to(idp_url) + end + + def auth_result + redirect_to('/') + end + + def logout + redirect_to(logout_uri) + end + + def authorization_url(ial:, aal: nil) + authorization_endpoint = openid_configuration[:authorization_endpoint] + params = ial2_params( + client_id: client_id, + acr_values: acr_values(ial: ial, aal: aal), + biometric_comparison_required: ial == BIOMETRIC_REQUIRED, + state: random_value, + nonce: random_value, + ) + request_params = params.merge( + scope: scopes_for(ial), + redirect_uri: test_oidc_auth_result_url, + ).compact.to_query + "#{authorization_endpoint}?#{request_params}" + end + + def prepare_step_up_flow(ial:) + if ial == 'step-up' + ial = '1' + end + ial + end + + def scopes_for(ial) + case ial + when '0' + 'openid email social_security_number' + when '1', nil + 'openid email' + when '2', BIOMETRIC_REQUIRED + 'openid email profile social_security_number phone address' + else + raise ArgumentError.new("Unexpected IAL: #{ial.inspect}") + end + end + + def acr_values(ial:, aal:) + ial_value = { + '0' => 'http://idmanagement.gov/ns/assurance/ial/0', + nil => 'http://idmanagement.gov/ns/assurance/ial/1', + '' => 'http://idmanagement.gov/ns/assurance/ial/1', + '1' => 'http://idmanagement.gov/ns/assurance/ial/1', + '2' => 'http://idmanagement.gov/ns/assurance/ial/2', + 'biometric-comparison-required' => 'http://idmanagement.gov/ns/assurance/ial/2', + }[ial] + aal_value = { + '2' => 'http://idmanagement.gov/ns/assurance/aal/2', + '2-phishing_resistant' => 'http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true', + '2-hspd12' => 'http://idmanagement.gov/ns/assurance/aal/2?hspd12=true', + }[aal] + [ial_value, aal_value].compact.join(' ') + end + + def json(response) + JSON.parse(response.to_s).with_indifferent_access + end + + def random_value + SecureRandom.hex + end + + def client_id + @client_id + end + + private + + def logout_uri + endpoint = openid_configuration[:end_session_endpoint] + request_params = { + client_id: client_id, + post_logout_redirect_uri: '/', + state: SecureRandom.hex, + }.to_query + + "#{endpoint}?#{request_params}" + end + + def openid_configuration + @openid_configuration ||= OpenidConnectConfigurationPresenter.new.configuration + end + + def idp_public_key + @idp_public_key ||= load_idp_public_key + end + + def load_idp_public_key + keys = OpenidConnectCertsPresenter.new.certs[:keys] + JSON::JWK.new(keys.first).to_key + end + + def update_service_provider + return @service_provider if defined?(@service_provider) + @service_provider = ServiceProvider.find_by(issuer: client_id) + # inject root url + changed = false + [test_oidc_logout_url, test_oidc_auth_result_url, root_url].each do |url| + if @service_provider&.redirect_uris && !@service_provider.redirect_uris.include?(url) + @service_provider.redirect_uris.append(url) + changed = true + end + end + @service_provider.save! if changed + end + end +end diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index df9c15076d9..f4cd119bc25 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -4,6 +4,7 @@ class WebauthnVerificationController < ApplicationController include TwoFactorAuthenticatable before_action :check_sp_required_mfa + before_action :check_if_device_supports_platform_auth, only: :show before_action :confirm_webauthn_enabled, only: :show def show @@ -33,6 +34,17 @@ def confirm private + def check_if_device_supports_platform_auth + return unless user_session.has_key?(:platform_authenticator_available) + if platform_authenticator? && !device_supports_webauthn_platform? + redirect_to login_two_factor_options_url + end + end + + def device_supports_webauthn_platform? + user_session.delete(:platform_authenticator_available) == true + end + def handle_webauthn_result(result) if result.success? handle_valid_webauthn diff --git a/app/controllers/users/auth_app_controller.rb b/app/controllers/users/auth_app_controller.rb new file mode 100644 index 00000000000..2082362dc04 --- /dev/null +++ b/app/controllers/users/auth_app_controller.rb @@ -0,0 +1,65 @@ +module Users + class AuthAppController < ApplicationController + include ReauthenticationRequiredConcern + + before_action :confirm_two_factor_authenticated + before_action :confirm_recently_authenticated_2fa + before_action :set_form + before_action :validate_configuration_exists + + def edit; end + + def update + result = form.submit(name: params.dig(:form, :name)) + + analytics.auth_app_update_name_submitted(**result.to_h) + + if result.success? + flash[:success] = t('two_factor_authentication.auth_app.renamed') + redirect_to account_path + else + flash.now[:error] = result.first_error_message + render :edit + end + end + + def destroy + result = form.submit + + analytics.auth_app_delete_submitted(**result.to_h) + + if result.success? + flash[:success] = t('two_factor_authentication.auth_app.deleted') + create_user_event(:authenticator_disabled) + revoke_remember_device(current_user) + event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user) + PushNotification::HttpPush.deliver(event) + redirect_to account_path + else + flash[:error] = result.first_error_message + redirect_to edit_auth_app_path(id: params[:id]) + end + end + + private + + def form + @form ||= form_class.new(user: current_user, configuration_id: params[:id]) + end + + alias_method :set_form, :form + + def form_class + case action_name + when 'edit', 'update' + TwoFactorAuthentication::AuthAppUpdateForm + when 'destroy' + TwoFactorAuthentication::AuthAppDeleteForm + end + end + + def validate_configuration_exists + render_not_found if form.configuration.blank? + end + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 7b019aa33da..651c8258c8d 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -121,6 +121,8 @@ def handle_valid_authentication user_id: current_user.id, email: auth_params[:email], ) + user_session[:platform_authenticator_available] = + params[:platform_authenticator_available] == 'true' redirect_to next_url_after_valid_authentication end diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index d3b1602f3f1..ec8980a3a9f 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -1,6 +1,7 @@ module Users class TwoFactorAuthenticationController < ApplicationController include TwoFactorAuthenticatable + include ApplicationHelper include ActionView::Helpers::DateHelper before_action :check_remember_device_preference diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index e71ffc0fd2b..a9b64774af4 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -85,7 +85,8 @@ def confirm if result.success? process_valid_webauthn(form) else - process_invalid_webauthn(form) + flash.now[:error] = result.first_error_message + render :new end end @@ -205,24 +206,6 @@ def need_to_set_up_additional_mfa? in_multi_mfa_selection_flow? && mfa_selection_count < 2 end - def process_invalid_webauthn(form) - if form.name_taken - if form.platform_authenticator? - flash.now[:error] = t('errors.webauthn_platform_setup.unique_name') - else - flash.now[:error] = t('errors.webauthn_setup.unique_name') - end - elsif form.platform_authenticator? - flash[:error] = t('errors.webauthn_platform_setup.general_error') - else - flash[:error] = t( - 'errors.webauthn_setup.general_error_html', - link_html: t('errors.webauthn_setup.additional_methods_link'), - ) - end - render :new - end - def new_params params.permit(:platform, :error) end diff --git a/app/forms/two_factor_authentication/auth_app_delete_form.rb b/app/forms/two_factor_authentication/auth_app_delete_form.rb new file mode 100644 index 00000000000..6a49f4513e4 --- /dev/null +++ b/app/forms/two_factor_authentication/auth_app_delete_form.rb @@ -0,0 +1,57 @@ +module TwoFactorAuthentication + class AuthAppDeleteForm + include ActiveModel::Model + include ActionView::Helpers::TranslationHelper + + attr_reader :user, :configuration_id + + validate :validate_configuration_exists + validate :validate_has_multiple_mfa + + def initialize(user:, configuration_id:) + @user = user + @configuration_id = configuration_id + end + + def submit + success = valid? + + configuration.destroy if success + + FormResponse.new( + success:, + errors:, + extra: extra_analytics_attributes, + serialize_error_details_only: true, + ) + end + + def configuration + @configuration ||= user.auth_app_configurations.find_by(id: configuration_id) + end + + private + + def validate_configuration_exists + return if configuration.present? + errors.add( + :configuration_id, + :configuration_not_found, + message: t('errors.manage_authenticator.internal_error'), + ) + end + + def validate_has_multiple_mfa + return if !configuration || MfaPolicy.new(user).multiple_factors_enabled? + errors.add( + :configuration_id, + :only_method, + message: t('errors.manage_authenticator.remove_only_method_error'), + ) + end + + def extra_analytics_attributes + { configuration_id: } + end + end +end diff --git a/app/forms/two_factor_authentication/auth_app_update_form.rb b/app/forms/two_factor_authentication/auth_app_update_form.rb new file mode 100644 index 00000000000..cd6bb2dbcbb --- /dev/null +++ b/app/forms/two_factor_authentication/auth_app_update_form.rb @@ -0,0 +1,68 @@ +module TwoFactorAuthentication + class AuthAppUpdateForm + include ActiveModel::Model + include ActionView::Helpers::TranslationHelper + + attr_reader :user, :configuration_id + + validate :validate_configuration_exists + validate :validate_unique_name + + def initialize(user:, configuration_id:) + @user = user + @configuration_id = configuration_id + end + + def submit(name:) + @name = name + + success = valid? + if valid? + configuration.name = name + success = configuration.valid? + errors.merge!(configuration.errors) + configuration.save if success + end + + FormResponse.new( + success:, + errors:, + extra: extra_analytics_attributes, + serialize_error_details_only: true, + ) + end + + def name + return @name if defined?(@name) + @name = configuration&.name + end + + def configuration + @configuration ||= user.auth_app_configurations.find_by(id: configuration_id) + end + + private + + def validate_configuration_exists + return if configuration.present? + errors.add( + :configuration_id, + :configuration_not_found, + message: t('errors.manage_authenticator.internal_error'), + ) + end + + def validate_unique_name + return unless user.auth_app_configurations.where.not(id: configuration_id).find_by(name:) + errors.add( + :name, + :duplicate, + message: t('errors.manage_authenticator.unique_name_error'), + ) + end + + def extra_analytics_attributes + { configuration_id: } + end + end +end diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb index 55ea451c109..84678797690 100644 --- a/app/forms/webauthn_setup_form.rb +++ b/app/forms/webauthn_setup_form.rb @@ -1,14 +1,15 @@ class WebauthnSetupForm include ActiveModel::Model - validates :user, presence: true - validates :challenge, presence: true - validates :attestation_object, presence: true - validates :client_data_json, presence: true - validates :name, presence: true + validates :user, + :challenge, + :attestation_object, + :client_data_json, + :name, + presence: { message: proc { |object| object.send(:generic_error_message) } } validate :name_is_unique - attr_reader :attestation_response, :name_taken + attr_reader :attestation_response def initialize(user:, user_session:, device_name:) @user = user @@ -43,6 +44,17 @@ def platform_authenticator? !!@platform_authenticator end + def generic_error_message + if platform_authenticator? + I18n.t('errors.webauthn_platform_setup.general_error') + else + I18n.t( + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), + ) + end + end + private attr_reader :success, :transports, :invalid_transports @@ -71,8 +83,12 @@ def name_is_unique count @name = "#{@name} (#{num_existing_devices})" else - errors.add :name, I18n.t('errors.webauthn_setup.unique_name'), type: :unique_name - @name_taken = true + name_error = if platform_authenticator? + I18n.t('errors.webauthn_platform_setup.unique_name') + else + I18n.t('errors.webauthn_setup.unique_name') + end + errors.add :name, name_error, type: :unique_name end end @@ -85,20 +101,24 @@ def valid_attestation_response?(protocol) end def safe_response(original_origin) - @attestation_response.valid?(@challenge.pack('c*'), original_origin) + response = @attestation_response.valid?(@challenge.pack('c*'), original_origin) + add_attestation_error unless response + response rescue StandardError + add_attestation_error + false + end + + def add_attestation_error if @platform_authenticator - errors.add :name, I18n.t( - 'errors.webauthn_platform_setup.attestation_error', - link: MarketingSite.contact_url, - ), type: :attestation_error + errors.add :name, I18n.t('errors.webauthn_platform_setup.general_error'), + type: :attestation_error else errors.add :name, I18n.t( - 'errors.webauthn_setup.attestation_error', - link: MarketingSite.contact_url, + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), ), type: :attestation_error end - false end def process_authenticator_data_value(data_value) diff --git a/app/javascript/packages/webauthn/index.ts b/app/javascript/packages/webauthn/index.ts index 7539dcc2507..a21ec105f22 100644 --- a/app/javascript/packages/webauthn/index.ts +++ b/app/javascript/packages/webauthn/index.ts @@ -2,6 +2,8 @@ export { default as enrollWebauthnDevice } from './enroll-webauthn-device'; export { default as extractCredentials } from './extract-credentials'; export { default as verifyWebauthnDevice } from './verify-webauthn-device'; export { default as isExpectedWebauthnError } from './is-expected-error'; +export { default as isWebauthnPlatformAuthenticatorAvailable } from './is-webauthn-platform-authenticator-available'; +export { default as isWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; export * from './converters'; export type { VerifyCredentialDescriptor } from './verify-webauthn-device'; diff --git a/app/javascript/packs/platform-authenticator-available.ts b/app/javascript/packs/platform-authenticator-available.ts new file mode 100644 index 00000000000..e77e7195b70 --- /dev/null +++ b/app/javascript/packs/platform-authenticator-available.ts @@ -0,0 +1,20 @@ +import { + isWebauthnPlatformAuthenticatorAvailable, + isWebauthnPasskeySupported, +} from '@18f/identity-webauthn'; + +async function platformAuthenticatorAvailable() { + const platformAuthenticatorAvailableInput = document.getElementById( + 'platform_authenticator_available', + ) as HTMLInputElement; + if (!platformAuthenticatorAvailableInput) { + return; + } + if (isWebauthnPasskeySupported() && (await isWebauthnPlatformAuthenticatorAvailable())) { + platformAuthenticatorAvailableInput.value = 'true'; + } else { + platformAuthenticatorAvailableInput.value = 'false'; + } +} + +platformAuthenticatorAvailable(); diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 60e74186606..b1934f1b620 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -19,8 +19,7 @@ def perform( encrypted_arguments:, trace_id:, should_proof_state_id:, - double_address_verification: nil, - ipp_enrollment_in_progress: false, + ipp_enrollment_in_progress:, user_id: nil, threatmetrix_session_id: nil, request_ip: nil, @@ -46,7 +45,6 @@ def perform( threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, should_proof_state_id: should_proof_state_id, - double_address_verification: double_address_verification, ipp_enrollment_in_progress: ipp_enrollment_in_progress, instant_verify_ab_test_discriminator: instant_verify_ab_test_discriminator, ) @@ -75,7 +73,6 @@ def make_vendor_proofing_requests( threatmetrix_session_id:, request_ip:, should_proof_state_id:, - double_address_verification:, ipp_enrollment_in_progress:, instant_verify_ab_test_discriminator: ) @@ -85,7 +82,6 @@ def make_vendor_proofing_requests( threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, should_proof_state_id: should_proof_state_id, - double_address_verification: double_address_verification, ipp_enrollment_in_progress: ipp_enrollment_in_progress, timer: timer, ) diff --git a/app/models/disposable_domain.rb b/app/models/disposable_domain.rb deleted file mode 100644 index 082199f0970..00000000000 --- a/app/models/disposable_domain.rb +++ /dev/null @@ -1,9 +0,0 @@ -class DisposableDomain < ApplicationRecord - class << self - def disposable?(domain) - return false if !domain.is_a?(String) || domain.empty? - - exists?(name: domain) - end - end -end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index f1956723085..223af635040 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -188,6 +188,45 @@ def add_phone_setup_visit ) end + # Tracks when a user deletes their auth app from account + # @param [Boolean] success + # @param [Hash] error_details + # @param [Integer] configuration_id + def auth_app_delete_submitted( + success:, + configuration_id:, + error_details: nil, + **extra + ) + track_event( + :auth_app_delete_submitted, + success:, + error_details:, + configuration_id:, + **extra, + ) + end + + # When a user updates name for auth app + # @param [Boolean] success + # @param [Hash] error_details + # @param [Integer] configuration_id + # Tracks when user submits a name change for an Auth App configuration + def auth_app_update_name_submitted( + success:, + configuration_id:, + error_details: nil, + **extra + ) + track_event( + :auth_app_update_name_submitted, + success:, + error_details:, + configuration_id:, + **extra, + ) + end + # When a user views the "you are already signed in with the following email" screen def authentication_confirmation track_event('Authentication Confirmation') @@ -4531,6 +4570,7 @@ def user_registration_cancellation(request_came_from:, **extra) # @param [String] needs_completion_screen_reason # @param [Array] sp_request_requested_attributes # @param [Array] sp_session_requested_attributes + # @param [String, nil] disposable_email_domain Disposable email domain used for registration def user_registration_complete( ial2:, service_provider_name:, @@ -4539,6 +4579,7 @@ def user_registration_complete( sp_session_requested_attributes:, sp_request_requested_attributes: nil, ialmax: nil, + disposable_email_domain: nil, **extra ) track_event( @@ -4550,6 +4591,7 @@ def user_registration_complete( needs_completion_screen_reason: needs_completion_screen_reason, sp_request_requested_attributes: sp_request_requested_attributes, sp_session_requested_attributes: sp_session_requested_attributes, + disposable_email_domain: disposable_email_domain, **extra, ) end diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index 0bdf08488c2..7d2e411e7f1 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -246,7 +246,8 @@ def all_passed? transaction_status_passed? && true_id_product.present? && product_status_passed? && - doc_auth_result_passed? + doc_auth_result_passed? && + (@liveness_checking_enabled ? selfie_success : true) end def selfie_result diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index fd3d6524655..2c9a24bebd1 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -52,8 +52,7 @@ def errors # Error generator is not to be called when it's not failure # allows us to test successful results return {} if all_doc_capture_values_passing?( - doc_auth_result, id_type_supported?, - face_match_result + doc_auth_result, id_type_supported? ) mock_args = {} @@ -178,10 +177,10 @@ def doc_auth_result_from_success end end - def all_doc_capture_values_passing?(doc_auth_result, id_type_supported, face_match_result) + def all_doc_capture_values_passing?(doc_auth_result, id_type_supported) doc_auth_result == 'Passed' && id_type_supported && - (@selfie_check_performed ? face_match_result == 'Pass' : true) + (@selfie_check_performed ? selfie_success : true) end def parse_uri diff --git a/app/services/idv/agent.rb b/app/services/idv/agent.rb index 045af77a857..abad581798c 100644 --- a/app/services/idv/agent.rb +++ b/app/services/idv/agent.rb @@ -11,7 +11,7 @@ def proof_resolution( user_id:, threatmetrix_session_id:, request_ip:, - ipp_enrollment_in_progress: false + ipp_enrollment_in_progress: ) document_capture_session.create_proofing_session @@ -28,7 +28,6 @@ def proof_resolution( user_id: user_id, threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, - double_address_verification: ipp_enrollment_in_progress, ipp_enrollment_in_progress: ipp_enrollment_in_progress, } diff --git a/app/services/proofing/resolution/progressive_proofer.rb b/app/services/proofing/resolution/progressive_proofer.rb index 791a723733e..eddbd58229b 100644 --- a/app/services/proofing/resolution/progressive_proofer.rb +++ b/app/services/proofing/resolution/progressive_proofer.rb @@ -29,8 +29,7 @@ def proof( threatmetrix_session_id:, timer:, user_email:, - double_address_verification: nil, - ipp_enrollment_in_progress: false + ipp_enrollment_in_progress: ) device_profiling_result = proof_with_threatmetrix_if_needed( applicant_pii: applicant_pii, @@ -43,7 +42,6 @@ def proof( residential_instant_verify_result = proof_residential_address_if_needed( applicant_pii: applicant_pii, timer: timer, - double_address_verification: double_address_verification, ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) @@ -56,7 +54,6 @@ def proof( applicant_pii: applicant_pii_transformed, timer: timer, residential_instant_verify_result: residential_instant_verify_result, - double_address_verification: double_address_verification, ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) @@ -66,13 +63,11 @@ def proof( residential_instant_verify_result: residential_instant_verify_result, instant_verify_result: instant_verify_result, should_proof_state_id: should_proof_state_id, - double_address_verification: double_address_verification, ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) ResultAdjudicator.new( device_profiling_result: device_profiling_result, - double_address_verification: double_address_verification, ipp_enrollment_in_progress: ipp_enrollment_in_progress, resolution_result: instant_verify_result, should_proof_state_id: should_proof_state_id, @@ -111,11 +106,9 @@ def proof_with_threatmetrix_if_needed( end end - # rubocop:disable Lint/UnusedMethodArgument def proof_residential_address_if_needed( applicant_pii:, timer:, - double_address_verification: false, ipp_enrollment_in_progress: false ) return residential_address_unnecessary_result unless ipp_enrollment_in_progress @@ -124,7 +117,6 @@ def proof_residential_address_if_needed( resolution_proofer.proof(applicant_pii) end end - # rubocop:enable Lint/UnusedMethodArgument def residential_address_unnecessary_result Proofing::Resolution::Result.new( @@ -138,10 +130,8 @@ def resolution_cannot_pass ) end - # rubocop:disable Lint/UnusedMethodArgument def proof_id_address_with_lexis_nexis_if_needed(applicant_pii:, timer:, residential_instant_verify_result:, - double_address_verification:, ipp_enrollment_in_progress:) if applicant_pii[:same_address_as_id] == 'true' && ipp_enrollment_in_progress return residential_instant_verify_result @@ -155,10 +145,9 @@ def proof_id_address_with_lexis_nexis_if_needed(applicant_pii:, timer:, def should_proof_state_id_with_aamva?(ipp_enrollment_in_progress:, same_address_as_id:, should_proof_state_id:, instant_verify_result:, - residential_instant_verify_result:, - double_address_verification:) + residential_instant_verify_result:) return false unless should_proof_state_id - # If the user is in double-address-verification and they have changed their address then + # If the user is in in-person-proofing and they have changed their address then # they are not eligible for get-to-yes if !ipp_enrollment_in_progress || same_address_as_id == 'true' user_can_pass_after_state_id_check?(instant_verify_result) @@ -166,19 +155,16 @@ def should_proof_state_id_with_aamva?(ipp_enrollment_in_progress:, same_address_ residential_instant_verify_result.success? end end - # rubocop:enable Lint/UnusedMethodArgument def proof_id_with_aamva_if_needed( applicant_pii:, timer:, residential_instant_verify_result:, instant_verify_result:, should_proof_state_id:, - ipp_enrollment_in_progress:, - double_address_verification: + ipp_enrollment_in_progress: ) same_address_as_id = applicant_pii[:same_address_as_id] should_proof_state_id_with_aamva = should_proof_state_id_with_aamva?( - double_address_verification:, ipp_enrollment_in_progress:, same_address_as_id:, should_proof_state_id:, diff --git a/app/services/proofing/resolution/result_adjudicator.rb b/app/services/proofing/resolution/result_adjudicator.rb index 3d9a05224d4..0d71af7f763 100644 --- a/app/services/proofing/resolution/result_adjudicator.rb +++ b/app/services/proofing/resolution/result_adjudicator.rb @@ -2,8 +2,7 @@ module Proofing module Resolution class ResultAdjudicator attr_reader :resolution_result, :state_id_result, :device_profiling_result, - :double_address_verification, :ipp_enrollment_in_progress, - :residential_resolution_result, :same_address_as_id + :ipp_enrollment_in_progress, :residential_resolution_result, :same_address_as_id def initialize( resolution_result:, # InstantVerify @@ -12,14 +11,12 @@ def initialize( should_proof_state_id:, ipp_enrollment_in_progress:, device_profiling_result:, - same_address_as_id:, - double_address_verification: true + same_address_as_id: ) @resolution_result = resolution_result @state_id_result = state_id_result @should_proof_state_id = should_proof_state_id @ipp_enrollment_in_progress = ipp_enrollment_in_progress - @double_address_verification = double_address_verification @device_profiling_result = device_profiling_result @residential_resolution_result = residential_resolution_result @same_address_as_id = same_address_as_id # this is a string, "true" or "false" diff --git a/app/views/account_reset/recovery_options/show.html.erb b/app/views/account_reset/recovery_options/show.html.erb index 8b953527e27..8593e3a0e66 100644 --- a/app/views/account_reset/recovery_options/show.html.erb +++ b/app/views/account_reset/recovery_options/show.html.erb @@ -10,8 +10,9 @@ <%= c.with_item(heading: t('account_reset.recovery_options.use_device')) do %>

<%= t('account_reset.recovery_options.try_another_device') %>

<% end %> - <%= c.with_item(heading: t('account_reset.recovery_options.check_webauthn_platform')) do %> -

<%= t('account_reset.recovery_options.check_webauthn_platform_info', app_name: APP_NAME) %>

+ <%= c.with_item(heading: t('account_reset.recovery_options.check_saved_credential')) do %> +

<%= t('account_reset.recovery_options.check_webauthn_platform_info') %>

+

<%= t('account_reset.recovery_options.use_same_device') %>

<% end %> <% end %> diff --git a/app/views/accounts/_auth_apps.html.erb b/app/views/accounts/_auth_apps.html.erb index 64a39eaf59f..e13de333a53 100644 --- a/app/views/accounts/_auth_apps.html.erb +++ b/app/views/accounts/_auth_apps.html.erb @@ -1,22 +1,24 @@

<%= t('headings.account.authentication_apps') %>

-
- <% MfaContext.new(current_user).auth_app_configurations.each do |auth_app_configuration| %> -
-
-
- <%= auth_app_configuration.name %> -
-
- <% if MfaPolicy.new(current_user).multiple_factors_enabled? %> -
- <%= render 'accounts/actions/disable_totp', id: auth_app_configuration.id %> -
- <% end %> -
+ +
+ <% MfaContext.new(current_user).auth_app_configurations.each do |configuration| %> + <%= render ManageableAuthenticatorComponent.new( + configuration:, + user_session:, + manage_url: edit_auth_app_path(id: configuration.id), + manage_api_url: api_internal_two_factor_authentication_auth_app_path(id: configuration.id), + custom_strings: { + deleted: t('two_factor_authentication.auth_app.deleted'), + renamed: t('two_factor_authentication.auth_app.renamed'), + manage_accessible_label: t('two_factor_authentication.auth_app.manage_accessible_label'), + }, + role: 'list-item', + ) %> <% end %>
+ <% if current_user.auth_app_configurations.count < IdentityConfig.store.max_auth_apps_per_account %> <%= render ButtonComponent.new( action: ->(**tag_options, &block) do diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index f4550fd95ac..48c66b23065 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -47,6 +47,7 @@ }, }, ) %> + <%= hidden_field_tag :platform_authenticator_available, id: 'platform_authenticator_available' %> <%= f.submit t('links.sign_in'), full_width: true, wide: false %> <% end %> <% if @ial && desktop_device? %> @@ -86,3 +87,5 @@ <% end %> +<%= javascript_packs_tag_once('platform-authenticator-available') %> + diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb index 545c3886db9..0ce1009fd3c 100644 --- a/app/views/idv/welcome/show.html.erb +++ b/app/views/idv/welcome/show.html.erb @@ -1,107 +1,67 @@ -<% self.title = t('doc_auth.headings.welcome') %> - -<% content_for(:pre_flash_content) do %> - <%= render StepIndicatorComponent.new( - steps: Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS, - current_step: :getting_started, - locale_scope: 'idv', - class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', - ) %> -<% end %> - - <%= render JavascriptRequiredComponent.new( - header: t('idv.welcome.no_js_header'), - intro: t('idv.welcome.no_js_intro', sp_name: decorated_sp_session.sp_name || APP_NAME), - location: :idv_welcome, - ) do %> - - <%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.welcome')) %> -

- <%= t('doc_auth.info.welcome', sp_name: decorated_sp_session.sp_name || APP_NAME) %> -

+<% self.title = @title %> +<%= render JavascriptRequiredComponent.new( + header: t('idv.welcome.no_js_header'), + intro: t('idv.welcome.no_js_intro', sp_name: @sp_name), + location: :idv_welcome, + ) do %> +<%= render PageHeadingComponent.new.with_content(@title) %> +

+ <%= t( + 'doc_auth.info.getting_started_html', + sp_name: @sp_name, + link_html: new_tab_link_to( + t('doc_auth.info.getting_started_learn_more'), + help_center_redirect_path( + category: 'verify-your-identity', + article: 'how-to-verify-your-identity', + flow: :idv, + step: :welcome, + location: 'intro_paragraph', + ), + ), + ) %> +

-

<%= t('doc_auth.instructions.welcome') %>

+

<%= t('doc_auth.instructions.getting_started') %>

- <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> + <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> + <% if decorated_sp_session.selfie_required? %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet1_with_selfie')) do %> +

<%= t('doc_auth.instructions.text1_with_selfie') %>

+ <% end %> + <% else %> <%= c.with_item(heading: t('doc_auth.instructions.bullet1')) do %>

<%= t('doc_auth.instructions.text1') %>

<% end %> - <%= c.with_item(heading: t('doc_auth.instructions.bullet2')) do %> -

<%= t('doc_auth.instructions.text2') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.instructions.bullet3')) do %> - - <%= new_tab_link_to( - t('idv.troubleshooting.options.learn_more_address_verification_options'), - help_center_redirect_path( - category: 'verify-your-identity', - article: 'phone-number', - flow: :idv, - step: :welcome, - location: 'you_will_need', - ), - ) %> - <% end %> <% end %> - - <%= simple_form_for :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-y-5 js-consent-continue-form' } do |f| %> - <%= f.submit t('doc_auth.buttons.continue') %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet2')) do %> +

<%= t('doc_auth.instructions.text2') %>

<% end %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet3')) do %> +

<%= t('doc_auth.instructions.text3') %>

+ <% end %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet4', app_name: APP_NAME)) do %> +

<%= t('doc_auth.instructions.text4') %>

+ <% end %> + <% end %> +<%= simple_form_for( + :doc_auth, + url: url_for, + method: 'put', + html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' }, + ) do |f| %> - <%= render( - 'shared/troubleshooting_options', - heading_tag: :h3, - heading: t('idv.troubleshooting.headings.missing_required_items'), - options: [ - { - url: help_center_redirect_path( - category: 'verify-your-identity', - article: 'accepted-state-issued-identification', - flow: :idv, - step: :welcome, - location: 'missing_items', - ), - text: t('idv.troubleshooting.options.supported_documents'), - new_tab: true, - }, - { - url: help_center_redirect_path( - category: 'verify-your-identity', - article: 'phone-number', - flow: :idv, - step: :welcome, - location: 'missing_items', - ), - text: t('idv.troubleshooting.options.learn_more_address_verification_options'), - new_tab: true, - }, - decorated_sp_session.sp_name && { - url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), - text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_sp_session.sp_name), - new_tab: true, - }, - ].select(&:present?), - ) %> - -

<%= t('doc_auth.instructions.privacy') %>

-

- <%= t('doc_auth.info.privacy', app_name: APP_NAME) %> -

-

- <%= new_tab_link_to( - t('doc_auth.instructions.learn_more'), - policy_redirect_url(flow: :idv, step: :welcome, location: :footer), +

+ <%= render( + SpinnerButtonComponent.new( + type: :submit, + big: true, + wide: true, + spin_on_click: false, + ).with_content(t('doc_auth.buttons.continue')), ) %> -

- +
+<% end %> <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> <% end %> - <%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/test/oidc_test/index.html.erb b/app/views/test/oidc_test/index.html.erb new file mode 100644 index 00000000000..8b15eadb9e7 --- /dev/null +++ b/app/views/test/oidc_test/index.html.erb @@ -0,0 +1,7 @@ +<% self.title = 'OIDC Test Controller' %> + +

OIDC Test Controller

+ +<%= link_to 'Sign in with Biometric', @start_url_selfie, class: 'sign-in-bttn' %>

+<%= link_to 'Sign in with IAL2', @start_url_ial2, class: 'sign-in-bttn' %>

+<%= link_to 'Sign in with IAL1', @start_url_ial1, class: 'sign-in-bttn' %> diff --git a/app/views/users/auth_app/edit.html.erb b/app/views/users/auth_app/edit.html.erb new file mode 100644 index 00000000000..55bbe2409a5 --- /dev/null +++ b/app/views/users/auth_app/edit.html.erb @@ -0,0 +1,40 @@ +<% self.title = t('two_factor_authentication.auth_app.edit_heading') %> + +<%= render PageHeadingComponent.new.with_content(t('two_factor_authentication.auth_app.edit_heading')) %> + +<%= simple_form_for( + @form, + as: :form, + method: :put, + html: { autocomplete: 'off' }, + url: auth_app_path(id: @form.configuration.id), + ) do |f| %> + <%= render ValidatedFieldComponent.new( + form: f, + name: :name, + label: t('two_factor_authentication.auth_app.nickname'), + ) %> + + <%= f.submit( + t('two_factor_authentication.auth_app.change_nickname'), + class: 'display-block margin-top-5', + ) %> +<% end %> + +<%= render ButtonComponent.new( + action: ->(**tag_options, &block) do + button_to( + auth_app_path(id: @form.configuration.id), + form: { aria: { label: t('two_factor_authentication.auth_app.delete') } }, + **tag_options, + &block + ) + end, + method: :delete, + big: true, + wide: true, + danger: true, + class: 'display-block margin-top-2', + ).with_content(t('two_factor_authentication.auth_app.delete')) %> + +<%= render 'shared/cancel', link: account_path %> diff --git a/config/locales/account_reset/en.yml b/config/locales/account_reset/en.yml index 7e44407eb36..5e2bc1bf198 100644 --- a/config/locales/account_reset/en.yml +++ b/config/locales/account_reset/en.yml @@ -37,10 +37,11 @@ en: %{interval}, you will receive an email with instructions to complete the deletion. recovery_options: - check_webauthn_platform: Use the same device you first set up face or touch unlock with - check_webauthn_platform_info: If you set up face or touch unlock when you - created your account make sure to use the same device you created your - %{app_name} account on. + check_saved_credential: See if you have a saved credential + check_webauthn_platform_info: If you set up face or touch unlock, you may have + saved your credentials to a password manager, like iCloud Keychain or + Google Password Manager. Try using face or touch unlock on a browser + using that password manager. header: Are you sure you want to delete your account? help_text: If you’re locked out and still need access to %{app_name}, try these steps instead. @@ -48,6 +49,8 @@ en: “remember device” option. try_method_again: Try your authentication method again use_device: Use another device + use_same_device: Otherwise, try using the same device where you set up face or + touch unlock. request: are_you_sure: Are you sure you don’t have access to any of your authentication methods? delete_account: Delete your account diff --git a/config/locales/account_reset/es.yml b/config/locales/account_reset/es.yml index 6b31344231c..80b4214f4a3 100644 --- a/config/locales/account_reset/es.yml +++ b/config/locales/account_reset/es.yml @@ -38,11 +38,12 @@ es: %{interval}, recibirá un correo electrónico con instrucciones para completar la eliminación. recovery_options: - check_webauthn_platform: Utilice el mismo dispositivo con el que configuró el - desbloqueo facial o táctil por primera vez. - check_webauthn_platform_info: Si configuró el desbloqueo facial o táctil cuando - creó su cuenta, asegúrese de utilizar el mismo dispositivo con el que - creó su cuenta de %{app_name}. + check_saved_credential: Verifica si tienes una credencial almacenada + check_webauthn_platform_info: Si has habilitado el desbloqueo facial o táctil, + es probable que hayas guardado tus credenciales en una herramienta de + gestión de contraseñas, como iCloud Keychain o Google Password Manager. + Intenta realizar el desbloqueo facial o táctil en un navegador que + utilice ese gestor de contraseñas. header: '¿Seguro que desea eliminar su cuenta?' help_text: Si sigue sin poder acceder a %{app_name}, pruebe con estos pasos en su lugar. @@ -50,6 +51,8 @@ es: opción “Recordar dispositivo”. try_method_again: Pruebe nuevamente su método de autenticación. use_device: Utilice otro dispositivo. + use_same_device: De lo contrario, inténtalo con el mismo dispositivo en el que + configuraste el desbloqueo facial o táctil. request: are_you_sure: '¿Estás seguro de que no tienes acceso a ninguno de tus métodos de seguridad?' diff --git a/config/locales/account_reset/fr.yml b/config/locales/account_reset/fr.yml index 49afdbe4c53..90a29cbe5e0 100644 --- a/config/locales/account_reset/fr.yml +++ b/config/locales/account_reset/fr.yml @@ -38,12 +38,13 @@ fr: Dans %{interval}, vous recevrez un e-mail avec des instructions pour terminer la suppression. recovery_options: - check_webauthn_platform: Utilisez le même appareil que celui avec lequel vous - avez configuré le déverrouillage facial ou tactile + check_saved_credential: Vérifiez si vous avez des informations d’identification sauvegardées check_webauthn_platform_info: Si vous avez configuré le déverrouillage facial ou - tactile lorsque vous avez créé votre compte, assurez-vous d’utiliser le - même appareil que celui avec lequel vous avez créé votre compte - %{app_name}. + tactile, vous avez peut-être sauvegardé vos informations + d’identification dans un gestionnaire de mots de passe, tel que iCloud + Keychain ou Google Password Manager. Essayez d’utiliser le + déverrouillage facial ou tactile sur un navigateur utilisant ce + gestionnaire de mots de passe. header: Êtes-vous sûr de vouloir supprimer votre compte? help_text: Si vous êtes bloqué et que vous avez toujours besoin d’accéder à %{app_name}, essayez plutôt ces étapes. @@ -51,6 +52,8 @@ fr: sélectionné l’option « mémoriser l’appareil ». try_method_again: Essayez à nouveau votre méthode d’authentification use_device: Utilisez un autre appareil + use_same_device: Sinon, essayez d’utiliser le même appareil où vous avez + configuré le déverrouillage facial ou tactile. request: are_you_sure: Êtes-vous sûr de n’avoir accès à aucune de vos méthodes de sécurité? delete_account: Supprimer votre compte diff --git a/config/locales/countries/en.yml b/config/locales/countries/en.yml index d7660653bdc..c00745ccbba 100644 --- a/config/locales/countries/en.yml +++ b/config/locales/countries/en.yml @@ -24,7 +24,6 @@ en: bh: Bahrain bi: Burundi bj: Benin - bl: Berundi bm: Bermuda bn: Brunei bo: Bolivia diff --git a/config/locales/countries/es.yml b/config/locales/countries/es.yml index df70433b973..e01754156fe 100644 --- a/config/locales/countries/es.yml +++ b/config/locales/countries/es.yml @@ -24,7 +24,6 @@ es: bh: Bahrain bi: Burundi bj: Benin - bl: Berundi bm: Bermuda bn: Brunei bo: Bolivia diff --git a/config/locales/countries/fr.yml b/config/locales/countries/fr.yml index c6cfcc55912..b7b70b44fce 100644 --- a/config/locales/countries/fr.yml +++ b/config/locales/countries/fr.yml @@ -24,7 +24,6 @@ fr: bh: Bahrain bi: Burundi bj: Benin - bl: Berundi bm: Bermuda bn: Brunei bo: Bolivie diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 3bc08d364c2..40b0f6a3a9a 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -156,7 +156,6 @@ en: document_capture_subheader_selfie: Photo of yourself document_capture_with_selfie: Add photos of your ID and a photo of yourself front: Front of your driver’s license or state ID - getting_started: Let’s verify your identity for %{sp_name} how_to_verify: Choose how you want to verify your identity hybrid_handoff: How would you like to add your ID? interstitial: We are processing your images @@ -173,7 +172,7 @@ en: verify_at_post_office: Verify your identity at a Post Office verify_identity: Verify your identity verify_online: Verify your identity online - welcome: Get started verifying your identity + welcome: Let’s verify your identity for %{sp_name} hybrid_flow_warning: explanation_html: You’re using %{app_name} to verify your identity for access to %{service_provider_name} and its @@ -195,6 +194,9 @@ en: exit: with_sp: Exit %{app_name} and return to %{sp_name} without_sp: Exit identity verification and go to your account page + getting_started_html: '%{sp_name} needs to make sure you are you — not someone + pretending to be you. %{link_html}' + getting_started_learn_more: Learn more about verifying your identity how_to_verify: You have the option to verify your identity online, or in person at a participating Post Office how_to_verify_troubleshooting_options_header: Want to learn more about how to verify your identity? @@ -213,9 +215,6 @@ en: link_sent_complete_no_polling: When you are done, click Continue here to finish verifying your identity. link_sent_complete_polling: The next step will load automatically. no_ssn: You must have a Social Security number to finish verifying your identity. - privacy: '%{app_name} is a secure, government website that adheres to the - highest standards in data protection. We use your data to verify your - identity.' review_examples_of_photos: Review examples of how to take clear photos of your ID. secure_account: We’ll encrypt your account with your password. Encryption means your data is protected and only you will be able to access or change @@ -244,30 +243,30 @@ en: verify_online_instruction: You’ll take photos of your ID to verify your identity fully online. Most users finish this process in one sitting. verify_online_link_text: Learn more about verifying online - welcome: '%{sp_name} needs to make sure you are you — not someone pretending to - be you.' you_entered: 'You entered:' instructions: - bullet1: State‑issued ID - bullet2: Social Security number - bullet3: Phone number OR home address + bullet1: Take photos of your ID + bullet1_with_selfie: Take photos of yourself and your ID + bullet2: Enter your Social Security number + bullet3: Match to your phone number + bullet4: Re-enter your %{app_name} password consent: By checking this box, you are letting %{app_name} ask for, use, keep, and share your personal information. We will use it to verify your identity. + getting_started: 'You’ll need to:' learn_more: Learn more about our privacy and security measures - privacy: Our privacy and security standards switch_back: Switch back to your computer to finish verifying your identity. switch_back_image: Arrow pointing from phone to computer test_ssn: In the test environment only SSNs that begin with “900-” or “666-” are considered valid. Do not enter real PII in this field. - text1: Your ID cannot be expired. - text2: You will not need the card with you. - text3_html: - - 'Verify by phone: We’ll call or text your phone - number. This takes a few minutes.' - - 'Verify by mail: We’ll mail a letter to your home - address. This takes 5 to 10 days.' - welcome: 'You will need your:' + text1: Use your driver’s license or state ID card. Other forms of ID are not + accepted. + text1_with_selfie: Take a photo of yourself and take photos of your driver’s + license or state ID card. Other forms of ID are not accepted. + text2: You will not need your physical SSN card. + text3: Your phone number matches you to your personal information. After you + match, we’ll send you a code. + text4: Your password saves and encrypts your personal information. tips: document_capture_hint: Must be a JPG or PNG document_capture_id_text1: Use a dark background diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 9aa65026e33..ce537f99f04 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -187,7 +187,6 @@ es: document_capture_subheader_selfie: Foto suya document_capture_with_selfie: Incluir fotos de su identificación y una foto suya front: Anverso de su licencia de conducir o identificación estatal - getting_started: Vamos a verificar su identidad para %{sp_name} how_to_verify: Elija cómo quiere verificar su identidad hybrid_handoff: '¿Cómo desea añadir su documento de identidad?' interstitial: Estamos procesando sus imágenes @@ -204,7 +203,7 @@ es: verify_at_post_office: Verifique su identidad en una oficina de correos verify_identity: Verifique su identidad verify_online: Verifique su identidad en línea - welcome: Comience a verificar su identidad + welcome: Vamos a verificar tu identidad para %{sp_name} hybrid_flow_warning: explanation_html: Usted está utilizando %{app_name} para verificar su identidad y acceder a @@ -229,6 +228,9 @@ es: exit: with_sp: Salir de %{app_name} y volver a %{sp_name} without_sp: Salir de la verificación de identidad e ir a la página de su cuenta + getting_started_html: '%{sp_name} necesita asegurarse de que es usted y no es + alguien que se hace pasar por usted. %{link_html}' + getting_started_learn_more: Obtén más información sobre la verificación de tu identidad how_to_verify: Tiene la opción de verificar su identidad en línea o en persona en una oficina de correos participante. how_to_verify_troubleshooting_options_header: ¿Quiere saber más sobre cómo verificar su identidad? @@ -251,9 +253,6 @@ es: que verifiques tu identidad a través de tu teléfono. no_ssn: Debe tener un número de Seguro Social para finalizar la verificación de su identidad. - privacy: '%{app_name} es un sitio web gubernamental seguro que cumple con las - normas más estrictas de protección de datos. Utilizamos sus datos para - verificar su identidad.' review_examples_of_photos: Revisa ejemplos de cómo hacer fotos nítidas de tu documento de identidad. secure_account: Vamos a encriptar su cuenta con su contraseña. La encriptación significa que sus datos están protegidos y solo usted podrá acceder o @@ -286,31 +285,32 @@ es: verify_online_instruction: Tomará fotografías de tu identificación para verificar tu identidad completamente en línea. verify_online_link_text: Obtenga más información sobre la verificación en línea - welcome: '%{sp_name} necesita asegurarse de que es usted y no es alguien que se - hace pasar por usted.' you_entered: 'Ud. entregó:' instructions: - bullet1: Documento de identidad emitido por el estado. - bullet2: Número de seguro social - bullet3: Número de teléfono O domicilio + bullet1: Tomar fotos de tu identificación oficial + bullet1_with_selfie: Tomar fotos tuyas y de tu identificación oficial + bullet2: Ingresar tu Número de Seguro Social + bullet3: Confirmar tu número de teléfono + bullet4: Ingresar de nuevo tu contraseña de %{app_name} consent: Al marcar esta casilla, usted permite que %{app_name} solicite, utilice, conserve y comparta su información personal. Los utilizamos para verificar su identidad. + getting_started: 'Necesitarás:' learn_more: Obtenga más información sobre nuestras medidas de privacidad y seguridad - privacy: Nuestras normas de privacidad y seguridad switch_back: Regrese a su computadora para continuar con la verificación de su identidad. switch_back_image: Flecha que apunta del teléfono a la computadora test_ssn: En el entorno de prueba solo los SSN que comienzan con “900-” o “666-” se consideran válidos. No ingrese PII real en este campo. - text1: Su documento de identidad no puede estar caducado. - text2: No necesitará la tarjeta con usted. - text3_html: - - 'Verificar por teléfono: Le llamaremos o enviaremos - un mensaje de texto a su número de teléfono. Esto lleva unos minutos' - - 'Verificar por correo: Le enviaremos una carta a su - domicilio. Esto tarda entre 5 y 10 días.' - welcome: 'Necesitará su:' + text1: Utiliza tu licencia de conducir o identificación oficial estatal. No + aceptamos ningún otro tipo de identificación. + text1_with_selfie: Tómate una foto y toma fotografías de tu licencia de conducir + o identificación oficial estatal. No aceptamos ningún otro tipo de + identificación. + text2: No necesitarás tu tarjeta física del seguro social. + text3: Tu número de teléfono te vincula con tu información personal. Luego de + confirmar tu información, te enviaremos un código. + text4: Tu contraseña permitirá guardar y encriptar tu información personal. tips: document_capture_hint: Debe ser un JPG o PNG document_capture_id_text1: Use un fondo oscuro diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index f4111c0fe0b..1c12cc52be6 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -195,7 +195,6 @@ fr: document_capture_subheader_selfie: Photo de vous-même document_capture_with_selfie: Ajoutez des photos de votre pièce d’identité et une photo de vous-même front: Recto de votre permis de conduire ou de votre carte d’identité de l’État - getting_started: Vérifions votre identité pour %{sp_name} how_to_verify: Choisissez la manière dont vous souhaitez confirmer votre identité hybrid_handoff: Comment voulez-vous ajouter votre identifiant ? interstitial: Nous traitons vos images @@ -212,7 +211,7 @@ fr: verify_at_post_office: Confirmez votre identité un bureau de poste verify_identity: Vérifier votre identité verify_online: Confirmez votre identité en ligne - welcome: Commencez à vérifier votre identité + welcome: Vérifions votre identité pour %{sp_name} hybrid_flow_warning: explanation_html: Vous utilisez %{app_name} pour vérifier votre identité et accéder à %{service_provider_name} et à ses @@ -236,6 +235,9 @@ fr: exit: with_sp: Quittez %{app_name} et retournez à %{sp_name} without_sp: Quittez la vérification d’identité et accédez à la page de votre compte + getting_started_html: '%{sp_name} doit s’assurer que vous êtes bien vous, et non + quelqu’un qui se fait passer pour vous. %{link_html}' + getting_started_learn_more: Pour plus d’informations sur la vérification de votre identité how_to_verify: Vous avez la possibilité de confirmer votre identité en ligne ou en personne dans un bureau de poste participant. how_to_verify_troubleshooting_options_header: Vous voulez en savoir plus sur la façon de vérifier votre identité? @@ -260,9 +262,6 @@ fr: téléphone. no_ssn: Vous devez avoir un numéro de sécurité sociale pour terminer la vérification de votre identité. - privacy: '%{app_name} est un site gouvernemental sécurisé qui respecte les - normes les plus strictes en matière de protection des données. Nous - utilisons vos données pour vérifier votre identité.' review_examples_of_photos: Examinez des exemples de photos claires de votre pièce d’identité. secure_account: Nous chiffrerons votre compte avec votre mot de passe. Le chiffrage signifie que vos données sont protégées et que vous êtes le/la @@ -296,33 +295,32 @@ fr: verify_online_instruction: Vous prendrez des photos de votre pièce d’identité pour confirmer votre identité entièrement en ligne. verify_online_link_text: En savoir plus sur la confirmation en ligne - welcome: '%{sp_name} doit s’assurer que vous êtes bien vous, et non quelqu’un - qui se fait passer pour vous.' you_entered: 'Tu as soumis:' instructions: - bullet1: Carte d’identité délivrée par l’État - bullet2: Numéro de sécurité sociale - bullet3: Numéro de téléphone OU adresse du domicile + bullet1: Prendre des photos de votre pièce d’identité + bullet1_with_selfie: Prenez des photos de vous et de vos pièces d’identité + bullet2: Saisissez votre numéro de sécurité sociale + bullet3: Faites correspondre votre numéro de téléphone + bullet4: Saisissez à nouveau votre mot de passe %{app_name} consent: En cochant cette case, vous autorisez %{app_name} à demander, utiliser, conserver et partager vos renseignements personnels. Nous les utilisons pour vérifier votre identité. + getting_started: 'Vous aurez besoin de :' learn_more: En savoir plus sur nos mesures de confidentialité et de sécurité - privacy: Nos normes de confidentialité et de sécurité switch_back: Retournez sur votre ordinateur pour continuer à vérifier votre identité. switch_back_image: Flèche pointant du téléphone vers l’ordinateur test_ssn: Dans l’environnement de test seuls les SSN commençant par “900-” ou “900-” sont considérés comme valides. N’entrez pas de vrais PII dans ce champ. - text1: Votre carte d’identité ne doit pas être expirée. - text2: Vous n’aurez pas besoin de la carte sur vous. - text3_html: - - 'Vérification par téléphone : Nous appellerons ou - enverrons un SMS à votre numéro de téléphone. Cela prend quelques - minutes.' - - 'Vérification par courrier : Nous vous enverrons une - lettre à votre adresse personnelle. Cela prend 5 à 10 - jours.' - welcome: 'Vous aurez besoin de votre:' + text1: Utilisez votre permis de conduire ou votre carte d’identité nationale. + Les autres pièces d’identité ne sont pas acceptées. + text1_with_selfie: Prenez une photo de vous et une de votre permis de conduire + ou carte d’identité nationale. Les autres pièces d’identité ne sont pas + acceptées. + text2: Vous n’aurez pas besoin de votre carte de sécurité sociale. + text3: Votre numéro de téléphone correspond à vos informations personnelles. Une + fois la correspondance établie, nous vous enverrons un code. + text4: Votre mot de passe enregistre et crypte vos informations personnelles. tips: document_capture_hint: Doit être un JPG ou PNG document_capture_id_text1: Utilisez un fond sombre diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 677a871510f..e3861954239 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -124,10 +124,6 @@ en: account_setup_error: We were unable to add face or touch unlock. Please try again or %{link}. already_registered: Face or touch unlock is already registered on this device. Please try adding another authentication method. - attestation_error: Sorry, but your platform authenticator doesn’t appear to be a - FIDO platform authenticator. Please make sure your device is listed at - https://fidoalliance.org/certification/fido-certified-products/ and if - you believe this is our error, please contact at %{link}. choose_another_method: choose another authentication method general_error: We were unable to add face or touch unlock. Please try again or choose another method. @@ -138,10 +134,6 @@ en: webauthn_setup: additional_methods_link: choose another authentication method already_registered: Security key already registered. Please try a different security key. - attestation_error: Sorry, but your security key doesn’t appear to be a FIDO - security key. Please make sure your device is listed at - https://fidoalliance.org/certification/fido-certified-products/ and if - you believe this is our error, please contact at %{link}. general_error_html: We were unable to add the security key. Please try again or %{link_html}. not_supported: Your browser doesn’t support security keys. Use the latest version of Google Chrome, Microsoft Edge, Mozilla Firefox or Safari to diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index de0b30cc8f4..be20e24d5d1 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -134,11 +134,6 @@ es: already_registered: Ya está registrado el desbloqueo con la cara o con la huella digital en este dispositivo. Trate de agregar otro método de autenticación. - attestation_error: Lo sentimos, pero su desbloqueo facial o táctil no parece - funcionar. Por favor, asegúrese de que su dispositivo está registrado en - https://fidoalliance.org/certification/fido-certified-products/ y si - cree que se trata de un error nuestro, póngase en contacto con nosotros - en %{link}. choose_another_method: elija otro método de autenticación general_error: No pudimos agregar el desbloqueo con la cara o con la huella digital. Inténtelo de nuevo o elija otro método de autenticación. @@ -151,12 +146,6 @@ es: additional_methods_link: elija otro método de autenticación already_registered: Clave de seguridad ya registrada. Por favor, intente una clave de seguridad diferente. - attestation_error: Lo sentimos, pero su clave de seguridad no parece ser una - clave de seguridad FIDO. Por favor, asegúrese de que su dispositivo - aparezca en - https://fidoalliance.org/certification/fido-certified-products/. Si - considera que se trata de un error nuestro, póngase en contacto con - %{link}. general_error_html: No hemos podido añadir la clave de seguridad. Inténtelo de nuevo o %{link_html}. not_supported: Tu navegador no es compatible con llaves de seguridad. Utiliza la diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index e7274032aaa..fb52095e419 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -144,11 +144,6 @@ fr: already_registered: Le déverrouillage facial ou le déverrouillage tactile est déjà enregistré sur cet appareil. Veuillez essayer d’ajouter une autre méthode d’authentification. - attestation_error: Désolé, mais votre déverrouillage facial ou tactile ne semble - pas fonctionner. Veuillez vous assurer que votre appareil est répertorié - sur https://fidoalliance.org/certification/fido-certified-products/ et - si vous pensez qu’il s’agit d’une erreur de notre part, veuillez nous - contacter à %{link}. choose_another_method: choisir une autre méthode d’authentification general_error: Nous n’avons pas pu ajouter le déverrouillage facial ni le déverrouillage tactile. Veuillez réessayer ou choisir une autre méthode @@ -163,11 +158,6 @@ fr: additional_methods_link: choisir une autre méthode d’authentification already_registered: Clé de sécurité déjà enregistrée. Veuillez essayer une clé de sécurité différente. - attestation_error: Désolé, votre clé de sécurité ne semble pas être une clé de - sécurité FIDO. Veuillez vérifier que votre appareil est répertorié sur - https://fidoalliance.org/certification/fido-certified-products/. Si vous - pensez qu’il s’agit d’une erreur de notre part, veuillez contacter - %{link}. general_error_html: Nous n’avons pas pu ajouter la clé de sécurité. Veuillez réessayer ou %{link_html}. not_supported: Votre navigateur ne prend pas en charge les clés de sécurité. diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 8f43f13463e..2b70b1cba1c 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -299,13 +299,11 @@ en: unavailable: 'We are working to resolve an error' troubleshooting: headings: - missing_required_items: Are you missing one of these items? need_assistance: 'Need immediate assistance? Here’s how to get help:' options: contact_support: Contact %{app_name} Support doc_capture_tips: Tips for taking clear photos of your ID get_help_at_sp: Get help at %{sp_name} - learn_more_address_verification_options: Learn more about verifying by phone or mail learn_more_verify_by_mail: Learn more about verifying your address by mail learn_more_verify_by_phone: Learn more about what phone number to use learn_more_verify_by_phone_in_person: Learn more about verifying your phone number diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index 9ef6880606f..9144315a446 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -317,13 +317,11 @@ es: unavailable: Estamos trabajando para resolver un error troubleshooting: headings: - missing_required_items: '¿Le falta alguno de estos puntos?' need_assistance: '¿Necesita ayuda inmediata? Así es como puede obtener ayuda:' options: contact_support: Póngase en contacto con el servicio de asistencia de %{app_name} doc_capture_tips: Sugerencias para obtener fotos nítidas de tu documento de identidad get_help_at_sp: Obtenga ayuda en %{sp_name} - learn_more_address_verification_options: Obtenga más información sobre la verificación por teléfono o por correo learn_more_verify_by_mail: Obtenga más información sobre la verificación de su dirección por correo learn_more_verify_by_phone: Más información sobre qué número de teléfono usar diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index f7bc8a88251..8966a537d34 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -328,14 +328,12 @@ fr: unavailable: Nous travaillons à la résolution d’une erreur troubleshooting: headings: - missing_required_items: Est-ce qu’il vous manque un de ces éléments? need_assistance: 'Avez-vous besoin d’une assistance immédiate? Voici comment obtenir de l’aide:' options: contact_support: Contacter le service d’assistance de %{app_name} doc_capture_tips: Conseils pour prendre des photos claires de votre pièce d’identité get_help_at_sp: Demandez de l’aide à %{sp_name} - learn_more_address_verification_options: En savoir plus sur la vérification par téléphone ou par courrier learn_more_verify_by_mail: En savoir plus sur la vérification de votre adresse par courrier learn_more_verify_by_phone: Apprenez-en plus sur quel numéro de téléphone utiliser learn_more_verify_by_phone_in_person: En savoir plus sur la vérification de votre numéro de téléphone diff --git a/config/locales/two_factor_authentication/en.yml b/config/locales/two_factor_authentication/en.yml index 0dd6bd01859..e55ae0021a5 100644 --- a/config/locales/two_factor_authentication/en.yml +++ b/config/locales/two_factor_authentication/en.yml @@ -20,6 +20,14 @@ en: attempt_remaining_warning_html: one: You have %{count} attempt remaining. other: You have %{count} attempts remaining. + auth_app: + change_nickname: Change nickname + delete: Delete this device + deleted: Successfully deleted an authentication app method + edit_heading: Manage your authentication app settings + manage_accessible_label: Manage authentication app + nickname: Nickname + renamed: Successfully renamed your authentication app method backup_code_header_text: Enter your backup code backup_code_prompt: You can use this backup code once. After you submit it, you’ll need to use a new backup code next time. diff --git a/config/locales/two_factor_authentication/es.yml b/config/locales/two_factor_authentication/es.yml index 01ef2bc2814..5f094a6efae 100644 --- a/config/locales/two_factor_authentication/es.yml +++ b/config/locales/two_factor_authentication/es.yml @@ -20,6 +20,15 @@ es: attempt_remaining_warning_html: one: Le quedan %{count} intento. other: Le quedan %{count} intentos. + auth_app: + change_nickname: Cambiar apodo + delete: Eliminar este dispositivo + deleted: Se ha eliminado correctamente un método de aplicación de autenticación. + edit_heading: Gestionar la configuración de su aplicación de autenticación + manage_accessible_label: Gestionar la aplicación de autenticación + nickname: Apodo + renamed: Se ha cambiado correctamente el nombre de su método de aplicación de + autenticación. backup_code_header_text: Ingrese su código de respaldo backup_code_prompt: Puede utilizar este código de respaldo una vez. Tendrá que usar un nuevo código de respaldo la próxima vez después de que lo envíe. diff --git a/config/locales/two_factor_authentication/fr.yml b/config/locales/two_factor_authentication/fr.yml index bca8d4bb545..01465df3f19 100644 --- a/config/locales/two_factor_authentication/fr.yml +++ b/config/locales/two_factor_authentication/fr.yml @@ -22,6 +22,14 @@ fr: attempt_remaining_warning_html: one: Il vous reste %{count} tentative. other: Il vous reste %{count} tentatives. + auth_app: + change_nickname: Changer de pseudo + delete: Supprimer cet appareil + deleted: Suppression réussie d’une méthode d’application d’authentification + edit_heading: Gérer les paramètres de votre application d’authentification + manage_accessible_label: Gérer l’application d’authentification + nickname: Pseudo + renamed: Votre méthode d’application d’authentification a été renommée avec succès backup_code_header_text: Entrez votre code de sauvegarde backup_code_prompt: Vous pouvez utiliser ce code de sauvegarde une seule fois. Après l’avoir envoyé, vous devrez utiliser un nouveau code de sauvegarde diff --git a/config/routes.rb b/config/routes.rb index 78ab0a170d6..e806fcbeaa0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,6 +23,8 @@ namespace :two_factor_authentication do put '/webauthn/:id' => 'webauthn#update', as: :webauthn delete '/webauthn/:id' => 'webauthn#destroy', as: nil + put '/auth_app/:id' => 'auth_app#update', as: :auth_app + delete '/auth_app/:id' => 'auth_app#destroy', as: nil end end end @@ -147,6 +149,13 @@ get '/saml/decode_assertion' => 'saml_test#start' post '/saml/decode_assertion' => 'saml_test#decode_response' post '/saml/decode_slo_request' => 'saml_test#decode_slo_request' + + get '/oidc/login' => 'oidc_test#index' + get '/oidc' => 'oidc_test#start' + get '/oidc/auth_request' => 'oidc_test#auth_request' + get '/oidc/auth_result' => 'oidc_test#auth_result' + get '/oidc/logout' => 'oidc_test#logout' + get '/piv_cac_entry' => 'piv_cac_authentication_test_subject#new' post '/piv_cac_entry' => 'piv_cac_authentication_test_subject#create' @@ -255,7 +264,9 @@ get '/manage/webauthn/:id' => 'users/webauthn#edit', as: :edit_webauthn put '/manage/webauthn/:id' => 'users/webauthn#update', as: :webauthn delete '/manage/webauthn/:id' => 'users/webauthn#destroy', as: nil - + get '/manage/auth_app/:id' => 'users/auth_app#edit', as: :edit_auth_app + put '/manage/auth_app/:id' => 'users/auth_app#update', as: :auth_app + delete '/manage/auth_app/:id' => 'users/auth_app#destroy', as: nil get '/account/personal_key' => 'accounts/personal_keys#new', as: :create_new_personal_key post '/account/personal_key' => 'accounts/personal_keys#create' diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml index a00dcea2759..23a75d0a838 100644 --- a/config/service_providers.localdev.yml +++ b/config/service_providers.localdev.yml @@ -454,7 +454,6 @@ development: agency_id: 1 ial: 2 irs_attempts_api_enabled: true - push_notification_url: http://localhost:9292/api/push_notifications redirect_uris: - 'http://localhost:9292/' - 'http://localhost:9292/auth/result' @@ -542,6 +541,18 @@ development: - 'http://localhost:4000/' - 'http://localhost:4000/auth/result' + 'urn:gov:gsa:openidconnect:sp:test': + agency_id: 1 + ial: 2 + return_to_sp_url: 'http://localhost:3000' + redirect_uris: + - 'http://localhost:3000/' + - 'http://localhost:3000/test/oidc/auth_result' + certs: + - 'sp_sinatra_demo' + friendly_name: 'Example Test OIDC SP' + in_person_proofing_enabled: true + # These are fake production service providers needed for the # ServiceProviderSeeder tests. They are not actually used in production. # diff --git a/db/primary_migrate/20240110142935_drop_disposable_domains_table.rb b/db/primary_migrate/20240110142935_drop_disposable_domains_table.rb new file mode 100644 index 00000000000..5e0a0a33dea --- /dev/null +++ b/db/primary_migrate/20240110142935_drop_disposable_domains_table.rb @@ -0,0 +1,5 @@ +class DropDisposableDomainsTable < ActiveRecord::Migration[7.1] + def change + drop_table :disposable_domains + end +end diff --git a/db/schema.rb b/db/schema.rb index 669b7801e88..8cdcaf3f109 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_01_10_141229) do +ActiveRecord::Schema[7.1].define(version: 2024_01_10_142935) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" @@ -95,11 +95,6 @@ t.index ["user_id", "last_used_at"], name: "index_device_user_id_last_used_at" end - create_table "disposable_domains", force: :cascade do |t| - t.citext "name", null: false - t.index ["name"], name: "index_disposable_domains_on_name", unique: true - end - create_table "disposable_email_domains", force: :cascade do |t| t.citext "name", null: false t.index ["name"], name: "index_disposable_email_domains_on_name", unique: true diff --git a/docs/local-development.md b/docs/local-development.md index 923578765ca..bc560e8533f 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -64,7 +64,9 @@ asked to consent to share their information with the partner before being sent b To simulate a true end-to-end user experience, you can either... -- Use the built-in test controller for SAML logins at http://localhost:3000/test/saml/login +- Use the built-in test controller for SAML logins at http://localhost:3000/test/saml/login or OIDC logins at http://localhost:3000/test/oidc/login + + Note: to update service provider configurations, run the command `rake db:seed` or `make setup`. - Or, run a sample partner application, which is configured by default to run with your local IdP instance: - OIDC: https://github.com/18F/identity-oidc-sinatra - Runs at http://localhost:9292/ diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 03b45648ce5..b12c884dbcd 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -165,5 +165,6 @@ module Vendors MOCK_IDV_APPLICANT_FULL_STATE_ID_JURISDICTION = 'North Dakota' MOCK_IDV_APPLICANT_FULL_STATE = 'Montana' MOCK_IDV_APPLICANT_FULL_IDENTITY_DOC_ADDRESS_STATE = 'Virginia' + MOCK_IDV_APPLICANT_STATE = 'MT' end end diff --git a/lib/tasks/disposable_domains.rake b/lib/tasks/disposable_email_domains.rake similarity index 60% rename from lib/tasks/disposable_domains.rake rename to lib/tasks/disposable_email_domains.rake index 4310e90746d..9ecf648f51e 100644 --- a/lib/tasks/disposable_domains.rake +++ b/lib/tasks/disposable_email_domains.rake @@ -1,13 +1,13 @@ # rubocop:disable Rails/SkipsModelValidations require 'csv' -namespace :disposable_domains do - task :load, %i[s3_url] => [:environment] do |_task, args| +namespace :disposable_email_domains do + task :load, %i[s3_secrets_path] => [:environment] do |_task, args| # Need to increase statement timeout since command takes a long time. ActiveRecord::Base.connection.execute 'SET statement_timeout = 200000' - file = Identity::Hostdata.secrets_s3.read_file(args[:s3_url]) + file = Identity::Hostdata.secrets_s3.read_file(args[:s3_secrets_path]) names = file.split("\n") DisposableEmailDomain.insert_all(names.map { |name| { name: } }) end end -# rake "disposable_domains:load['URL_HERE']" +# rake "disposable_email_domains:load[S3_SECRETS_PATH]" # rubocop:enable Rails/SkipsModelValidations diff --git a/scripts/create-deploy-pr b/scripts/create-deploy-pr index a8623193067..310d4103beb 100755 --- a/scripts/create-deploy-pr +++ b/scripts/create-deploy-pr @@ -2,20 +2,43 @@ set -euo pipefail -ORIGIN=${ORIGIN:-origin} +GIT_REMOTE=${GIT_REMOTE:-origin} SOURCE=${SOURCE:-} DEPLOY_BRANCH=stages/prod PATCH=${PATCH:-} DRY_RUN=${DRY_RUN:-0} -CHANGELOG_FILE=${CHANGELOG_FILE:-.rc-changelog.md} +CHANGELOG_FILE=${CHANGELOG_FILE:-tmp/.rc-changelog.md} +FORMAT_CHANGELOG=${FORMAT_CHANGELOG:-} +GH_REPO=${GH_REPO:-18f/identity-idp} +STATUS_PROMOTION_LABEL='status - promotion' + +function check_gh_configuration { + if ! which gh > /dev/null 2>&1; then + echo "Github CLI (gh) is not installed. You can install it with: brew install gh" + exit 1 + fi -function get_last_rc { - GH_OUTPUT=$(gh release list --exclude-drafts --exclude-pre-releases --limit 1 || true) - if [ -z "$GH_OUTPUT" ]; then - echo "Failed to get latest released" >&2 + if [ "${CI:-}" == "1" ] && [ -z "${GH_TOKEN:-}" ]; then + # gh will not work in CI unless GH_TOKEN is explicitly set. + echo "You must set the GH_TOKEN environment variable." + exit 1 + fi + + # Verify our git remote aligns with GH configuration + GIT_REMOTE_REPO=$( + git remote get-url "$GIT_REMOTE" \ + | sed -E 's#(^https?://github.com/|^git@github.com:|\.git$)##g' \ + | tr '[:upper:]' '[:lower:]' \ + ) + + if [ "$GIT_REMOTE_REPO" != "$GH_REPO" ]; then + echo "\$GH_REPO is set to a different value ($GH_REPO) than the git remote ($GIT_REMOTE - $GIT_REMOTE_REPO) in use." exit 1 fi +} +function get_last_rc { + GH_OUTPUT=$(gh release list --exclude-drafts --exclude-pre-releases --limit 1 || true) LAST_RC=$(echo "$GH_OUTPUT" | grep -E --only-matching 'RC [0-9]+(\.[0-9]+)?' | sed 's/RC //') if [ -z "$LAST_RC" ]; then echo 0 @@ -53,18 +76,10 @@ function get_staging_sha { curl --silent https://idp.staging.login.gov/api/deploy.json | jq -r .git_sha } -if [ -z "${CI:-}" ]; then - echo "This script is meant to be run in a continuous integration environment." - exit 1 -fi - -if [ -z "${GH_TOKEN:-}" ] && [ "$DRY_RUN" == "0" ]; then - echo "You must set the GH_TOKEN environment variable." - exit 1 -fi +check_gh_configuration RC_BRANCH=stages/rc-$(date +'%Y-%m-%d') -if git rev-parse "$ORIGIN/$RC_BRANCH" > /dev/null 2>&1; then +if git rev-parse "$GIT_REMOTE/$RC_BRANCH" > /dev/null 2>&1; then echo "RC branch $RC_BRANCH already exists. Delete that branch and re-run this workflow to create a PR." >&2 exit 1 fi @@ -78,7 +93,7 @@ if [ -z "$SOURCE" ]; then echo "Staging currently running ${SHA}" else SHA=$(git rev-parse "$SOURCE" || true) - if [ -z $SHA ]; then + if [ -z "$SHA" ]; then echo "Invalid source: '$SOURCE'" exit 17 elif [ "$SOURCE" == "$SHA" ]; then @@ -88,18 +103,32 @@ else fi fi +mkdir -p "$(dirname "$CHANGELOG_FILE")" || true + echo "Building changelog..." -scripts/changelog_check.rb -s "$SHA" -b "${ORIGIN}/${DEPLOY_BRANCH}" > "$CHANGELOG_FILE" +scripts/changelog_check.rb -s "$SHA" -b "${GIT_REMOTE}/${DEPLOY_BRANCH}" > "$CHANGELOG_FILE" + +if [ "$FORMAT_CHANGELOG" != "" ]; then + # Pipe the changelog in as stdin to the hook + echo "Executing changelog formatting hook '${FORMAT_CHANGELOG}'..." + ORIGINAL_CHANGELOG_FILE="${CHANGELOG_FILE}.orig" + mv "$CHANGELOG_FILE" "$ORIGINAL_CHANGELOG_FILE" + cat "$ORIGINAL_CHANGELOG_FILE" | sh -c "$FORMAT_CHANGELOG" > "$CHANGELOG_FILE" + echo "Diff:" + diff --color=auto "$ORIGINAL_CHANGELOG_FILE" "$CHANGELOG_FILE" +fi if [[ $DRY_RUN -eq 0 ]]; then - echo "Pushing $RC_BRANCH to origin..." - git push $ORIGIN "$SHA:refs/heads/$RC_BRANCH" + echo "Pushing $RC_BRANCH to $GIT_REMOTE..." + git push "$GIT_REMOTE" "$SHA:refs/heads/$RC_BRANCH" + + gh label create "$STATUS_PROMOTION_LABEL" 2>/dev/null || true # Create PR echo "Creating PR..." gh pr create \ --title "Deploy RC ${NEXT_RC} to Production" \ - --label 'status - promotion' \ + --label "$STATUS_PROMOTION_LABEL" \ --base "$DEPLOY_BRANCH" \ --head "$RC_BRANCH" \ --body-file "$CHANGELOG_FILE" @@ -107,6 +136,4 @@ else echo "Dry run. Not creating PR." fi -echo "# Changelog" -cat "$CHANGELOG_FILE" && rm "$CHANGELOG_FILE" diff --git a/scripts/create-release b/scripts/create-release index 92ef2a73e42..f37f33a12d6 100755 --- a/scripts/create-release +++ b/scripts/create-release @@ -3,8 +3,10 @@ set -euo pipefail DEPLOY_BRANCH=stages/prod -PR_JSON_FILE=${PR_JSON_FILE:-.pr.json} -CHANGELOG_FILE=${CHANGELOG_FILE:-.changelog.md} +DRY_RUN=${DRY_RUN:-0} +PR_JSON_FILE=${PR_JSON_FILE:-tmp/.pr.json} +CHANGELOG_FILE=${CHANGELOG_FILE:-tmp/.changelog.md} +GH_REPO=${GH_REPO:-18f/identity-idp} USAGE=" ${0} [PULL_REQUEST_NUMBER] @@ -13,35 +15,54 @@ Creates a new release based on the given PR having been merged. " if [ $# -eq 0 ]; then - echo $USAGE + echo "$USAGE" exit 1 fi PR="$1"; shift -if [ -z "${CI:-}" ]; then - echo "This script is meant to be run in a continuous integration environment." +if ! which gh > /dev/null 2>&1; then + echo "Github CLI (gh) is not installed. You can install it with: brew install gh" exit 1 fi -if [ -z "${GH_TOKEN:-}" ]; then - echo "You must set the GH_TOKEN environment variable." - exit 1 +if [ "${CI:-}" == "1" ]; then + # gh will not work in CI unless GH_TOKEN is explicitly set. + if [ -z "${GH_TOKEN:-}" ]; then + echo "You must set the GH_TOKEN environment variable." + exit 1 + fi + + # GITHUB_SHA is provided by Github Actions, but if for some reason + # you are running locally with CI=1 trying to simulate a CI run, + # make it clear that we want it set. + if [ -z "${GITHUB_SHA:-}" ]; then + echo "\$GITHUB_SHA must be set." + exit 1 + fi fi +mkdir -p "$(dirname "$PR_JSON_FILE")" || true +mkdir -p "$(dirname "$CHANGELOG_FILE")" || true echo "Getting PR ${PR} data..." gh pr list \ - --json number,title,body \ + --json number,mergeCommit,title,body \ --base "$DEPLOY_BRANCH" \ --state merged \ - jq ".[] | select(.number == ${PR})" > "$PR_JSON_FILE" + | jq ".[] | select(.number == ${PR})" > "$PR_JSON_FILE" if [ ! -s "$PR_JSON_FILE" ]; then - echo "PR $PR not found." + echo "Merged PR $PR not found." exit 9 fi +if [ -z "${GITHUB_SHA:-}" ]; then + # $GITHUB_SHA is set by Github Actions. But when running locally we can + # pull it out of the PR's JSON blob. + GITHUB_SHA=$(jq --raw-output '.mergeCommit.oid' < "$PR_JSON_FILE") +fi + RC=$(jq --raw-output '.title' < "$PR_JSON_FILE" | sed -E 's/Deploy RC (.+) to .*/\1/') jq --raw-output '.body' < "$PR_JSON_FILE" > "$CHANGELOG_FILE" TITLE="RC $RC" @@ -49,7 +70,7 @@ TITLE="RC $RC" echo "Checking for existing release '$TITLE'..." EXISTING_RELEASE=$(gh release list --exclude-drafts | (grep "$TITLE" || true)) -if [ ! -z "$EXISTING_RELEASE" ]; then +if [ -n "$EXISTING_RELEASE" ]; then echo "❌ Release already exists: $TITLE" >&2 exit 10 else @@ -58,10 +79,14 @@ fi TAG=$(date -u +'%Y-%m-%dT%H%M%S') -echo "Creating release $TITLE with tag $TAG..." -gh release create \ - "$TAG" \ - --latest \ - --target "$GITHUB_SHA" \ - --title "$TITLE" \ - --notes-file "$CHANGELOG_FILE" +if [ "${DRY_RUN:-}" == "1" ]; then + echo "Dry run. Not creating release $TITLE with tag $TAG ($GITHUB_SHA)..." +else + echo "Creating release $TITLE with tag $TAG..." + gh release create \ + "$TAG" \ + --latest \ + --target "$GITHUB_SHA" \ + --title "$TITLE" \ + --notes-file "$CHANGELOG_FILE" +fi \ No newline at end of file diff --git a/spec/controllers/api/internal/two_factor_authentication/auth_app_controller_spec.rb b/spec/controllers/api/internal/two_factor_authentication/auth_app_controller_spec.rb new file mode 100644 index 00000000000..199a0e82a53 --- /dev/null +++ b/spec/controllers/api/internal/two_factor_authentication/auth_app_controller_spec.rb @@ -0,0 +1,217 @@ +require 'rails_helper' + +RSpec.describe Api::Internal::TwoFactorAuthentication::AuthAppController do + let(:user) { create(:user, :with_phone) } + let(:configuration) { create(:auth_app_configuration, user:) } + + before do + stub_analytics + stub_sign_in(user) if user + end + + describe '#update' do + let(:name) { 'example' } + let(:params) { { id: configuration.id, name: } } + let(:response) { put :update, params: params } + subject(:response_body) { JSON.parse(response.body, symbolize_names: true) } + + it 'responds with successful result' do + expect(response_body).to eq(success: true) + expect(response.status).to eq(200) + end + + it 'logs the submission attempt' do + response + + expect(@analytics).to have_logged_event( + :auth_app_update_name_submitted, + success: true, + error_details: nil, + configuration_id: configuration.id.to_s, + ) + end + + it 'includes csrf token in the response headers' do + expect(response.headers['X-CSRF-Token']).to be_kind_of(String) + end + + context 'signed out' do + let(:user) { nil } + let(:configuration) { create(:auth_app_configuration) } + + it 'responds with unauthorized response' do + expect(response_body).to eq(error: 'Unauthorized') + expect(response.status).to eq(401) + end + end + + context 'with invalid submission' do + let(:name) { '' } + + it 'responds with unsuccessful result' do + expect(response_body).to eq(success: false, error: t('errors.messages.blank')) + expect(response.status).to eq(400) + end + + it 'logs the submission attempt' do + response + + expect(@analytics).to have_logged_event( + :auth_app_update_name_submitted, + success: false, + configuration_id: configuration.id.to_s, + error_details: { name: { blank: true } }, + ) + end + end + + context 'not recently authenticated' do + before do + allow(controller).to receive(:recently_authenticated_2fa?).and_return(false) + end + + it 'responds with unauthorized response' do + expect(response_body).to eq(error: 'Unauthorized') + expect(response.status).to eq(401) + end + end + + context 'with a configuration that does not exist' do + let(:params) { { id: 0 } } + + it 'responds with unsuccessful result' do + expect(response_body).to eq( + success: false, + error: t('errors.manage_authenticator.internal_error'), + ) + expect(response.status).to eq(400) + end + end + + context 'with a configuration that does not belong to the user' do + let(:configuration) { create(:auth_app_configuration) } + + it 'responds with unsuccessful result' do + expect(response_body).to eq( + success: false, + error: t('errors.manage_authenticator.internal_error'), + ) + expect(response.status).to eq(400) + end + end + end + + describe '#destroy' do + let(:params) { { id: configuration.id } } + let(:response) { delete :destroy, params: params } + subject(:response_body) { JSON.parse(response.body, symbolize_names: true) } + + it 'responds with successful result' do + expect(response_body).to eq(success: true) + expect(response.status).to eq(200) + end + + it 'logs the submission attempt' do + response + + expect(@analytics).to have_logged_event( + :auth_app_delete_submitted, + success: true, + configuration_id: configuration.id.to_s, + error_details: nil, + ) + end + + it 'includes csrf token in the response headers' do + expect(response.headers['X-CSRF-Token']).to be_kind_of(String) + end + + it 'sends a recovery information changed event' do + expect(PushNotification::HttpPush).to receive(:deliver). + with(PushNotification::RecoveryInformationChangedEvent.new(user: user)) + + response + end + + it 'revokes remembered device' do + expect(user.remember_device_revoked_at).to eq nil + + freeze_time do + response + expect(user.reload.remember_device_revoked_at).to eq Time.zone.now + end + end + + it 'logs a user event for the removed credential' do + expect { response }.to change { user.events.authenticator_disabled.size }.by 1 + end + + context 'signed out' do + let(:user) { nil } + let(:configuration) { create(:auth_app_configuration) } + + it 'responds with unauthorized response' do + expect(response_body).to eq(error: 'Unauthorized') + expect(response.status).to eq(401) + end + end + + context 'with invalid submission' do + let(:user) { create(:user) } + + it 'responds with unsuccessful result' do + expect(response_body).to eq( + success: false, + error: t('errors.manage_authenticator.remove_only_method_error'), + ) + expect(response.status).to eq(400) + end + + it 'logs the submission attempt' do + response + + expect(@analytics).to have_logged_event( + :auth_app_delete_submitted, + success: false, + configuration_id: configuration.id.to_s, + error_details: { configuration_id: { only_method: true } }, + ) + end + end + + context 'not recently authenticated' do + before do + allow(controller).to receive(:recently_authenticated_2fa?).and_return(false) + end + + it 'responds with unauthorized response' do + expect(response_body).to eq(error: 'Unauthorized') + expect(response.status).to eq(401) + end + end + + context 'with a configuration that does not exist' do + let(:params) { { id: 0 } } + + it 'responds with unsuccessful result' do + expect(response_body).to eq( + success: false, + error: t('errors.manage_authenticator.internal_error'), + ) + expect(response.status).to eq(400) + end + end + + context 'with a configuration that does not belong to the user' do + let(:configuration) { create(:auth_app_configuration) } + + it 'responds with unsuccessful result' do + expect(response_body).to eq( + success: false, + error: t('errors.manage_authenticator.internal_error'), + ) + expect(response.status).to eq(400) + end + end + end +end diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 21f325de2bf..fcee63bdc1b 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -286,7 +286,7 @@ verified_attributes: [], ), device_profiling_result: Proofing::DdpResult.new(success: true), - ipp_enrollment_in_progress: false, + ipp_enrollment_in_progress: true, residential_resolution_result: Proofing::Resolution::Result.new(success: true), resolution_result: Proofing::Resolution::Result.new(success: true), same_address_as_id: true, diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb index fb149479dd5..102d3d4bc1e 100644 --- a/spec/controllers/sign_up/completions_controller_spec.rb +++ b/spec/controllers/sign_up/completions_controller_spec.rb @@ -263,6 +263,7 @@ sp_request_requested_attributes: nil, sp_session_requested_attributes: nil, in_account_creation_flow: true, + disposable_email_domain: nil, ) end diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index 622e104260f..e5e924b1183 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -184,12 +184,17 @@ def index expect(response).to redirect_to login_two_factor_webauthn_path(platform: false) end - it 'passes the platform parameter if the user has a platform autheticator' do - controller.current_user.webauthn_configurations.first.update!(platform_authenticator: true) + context 'when platform_authenticator' do + before do + controller.current_user.webauthn_configurations. + first.update!(platform_authenticator: true) + end - get :show + it 'passes the platform parameter if the user has a platform autheticator' do + get :show - expect(response).to redirect_to login_two_factor_webauthn_path(platform: true) + expect(response).to redirect_to login_two_factor_webauthn_path(platform: true) + end end end diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index afdd6dcdc48..efa3a03fbe9 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -400,10 +400,7 @@ 'Multi-Factor Authentication Setup', { enabled_mfa_methods_count: 0, - errors: { name: [I18n.t( - 'errors.webauthn_platform_setup.attestation_error', - link: MarketingSite.contact_url, - )] }, + errors: { name: [I18n.t('errors.webauthn_platform_setup.general_error')] }, error_details: { name: { attestation_error: true } }, in_account_creation_flow: false, mfa_method_counts: {}, diff --git a/spec/features/idv/doc_auth/welcome_spec.rb b/spec/features/idv/doc_auth/welcome_spec.rb index 23ca9a7629b..dd94e83f215 100644 --- a/spec/features/idv/doc_auth/welcome_spec.rb +++ b/spec/features/idv/doc_auth/welcome_spec.rb @@ -16,63 +16,17 @@ complete_doc_auth_steps_before_welcome_step end - it 'logs return to sp link click' do - click_on t('idv.troubleshooting.options.get_help_at_sp', sp_name: sp_name) - - expect(fake_analytics).to have_logged_event( - 'Return to SP: Failed to proof', - flow: nil, - location: 'missing_items', - redirect_url: instance_of(String), - step: 'welcome', - ) - end - - it 'logs supported documents troubleshooting link click' do - click_on t('idv.troubleshooting.options.supported_documents') - - expect(fake_analytics).to have_logged_event( - 'External Redirect', - step: 'welcome', - location: 'missing_items', - flow: 'idv', - redirect_url: MarketingSite.help_center_article_url( - category: 'verify-your-identity', - article: 'accepted-state-issued-identification', - ), - ) - end - - it 'logs missing items troubleshooting link click' do - within '.troubleshooting-options' do - click_on t('idv.troubleshooting.options.learn_more_address_verification_options') - end - - expect(fake_analytics).to have_logged_event( - 'External Redirect', - step: 'welcome', - location: 'missing_items', - flow: 'idv', - redirect_url: MarketingSite.help_center_article_url( - category: 'verify-your-identity', - article: 'phone-number', - ), - ) - end - - it 'logs "you will need" learn more link click' do - within '.usa-process-list' do - click_on t('idv.troubleshooting.options.learn_more_address_verification_options') - end + it 'logs "intro_paragraph" learn more link click' do + click_on t('doc_auth.info.getting_started_learn_more') expect(fake_analytics).to have_logged_event( 'External Redirect', step: 'welcome', - location: 'you_will_need', + location: 'intro_paragraph', flow: 'idv', redirect_url: MarketingSite.help_center_article_url( category: 'verify-your-identity', - article: 'phone-number', + article: 'how-to-verify-your-identity', ), ) end diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index ee0bfdc6667..778fc1d7462 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -5,6 +5,7 @@ include InPersonHelper let(:sp) { :oidc } + let(:sp_name) { 'Test SP' } scenario 'Unsupervised proofing happy path desktop' do try_to_skip_ahead_before_signing_in @@ -148,8 +149,7 @@ def validate_welcome_page expect(page).to have_current_path(idv_welcome_path) - # Check for expected content - expect_step_indicator_current_step(t('step_indicator.flows.idv.getting_started')) + expect(page).to have_content t('doc_auth.headings.welcome', sp_name: sp_name) end def validate_agreement_page @@ -215,12 +215,7 @@ def validate_ssn_page expect(page.find_field(t('idv.form.ssn_label'))['aria-invalid']).to eq('false') expect(page).to have_content(t('doc_auth.info.no_ssn')) - click_link( - t( - 'doc_auth.info.exit.with_sp', app_name: APP_NAME, - sp_name: 'Test SP' - ), - ) + click_link(t('doc_auth.info.exit.with_sp', app_name: APP_NAME, sp_name: sp_name)) expect(page).to have_current_path(idv_cancel_path(step: 'ssn_offramp')) click_on t('idv.cancel.actions.keep_going') diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index a1b8568f59e..d520a987931 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -420,155 +420,6 @@ end end - context 'transliteration' do - before(:each) do - allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled). - and_return(true) - end - - let(:user) { user_with_2fa } - let(:enrollment) { InPersonEnrollment.new } - - before do - allow(user).to receive(:establishing_in_person_enrollment). - and_return(enrollment) - end - - it 'shows validation errors', - allow_browser_log: true do - sign_in_and_2fa_user - begin_in_person_proofing - complete_prepare_step - complete_location_step - expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) - - fill_out_state_id_form_ok - fill_in t('in_person_proofing.form.state_id.first_name'), with: 'T0mmy "Lee"' - fill_in t('in_person_proofing.form.state_id.last_name'), with: 'Джейкоб' - fill_in t('in_person_proofing.form.state_id.address1'), with: '#1 $treet' - fill_in t('in_person_proofing.form.state_id.address2'), with: 'Gr@nd Lañe^' - fill_in t('in_person_proofing.form.state_id.city'), with: 'B3st C!ty' - click_idv_continue - - expect(page).to have_content( - I18n.t( - 'in_person_proofing.form.state_id.errors.unsupported_chars', - char_list: '", 0', - ), - ) - - expect(page).to have_content( - I18n.t( - 'in_person_proofing.form.state_id.errors.unsupported_chars', - char_list: 'Д, б, е, ж, й, к, о', - ), - ) - - expect(page).to have_content( - I18n.t( - 'in_person_proofing.form.state_id.errors.unsupported_chars', - char_list: '$', - ), - ) - - expect(page).to have_content( - I18n.t( - 'in_person_proofing.form.state_id.errors.unsupported_chars', - char_list: '@, ^', - ), - ) - - expect(page).to have_content( - I18n.t( - 'in_person_proofing.form.state_id.errors.unsupported_chars', - char_list: '!, 3', - ), - ) - - # re-fill state id form with good inputs - fill_in t('in_person_proofing.form.state_id.first_name'), - with: InPersonHelper::GOOD_FIRST_NAME - fill_in t('in_person_proofing.form.state_id.last_name'), - with: InPersonHelper::GOOD_LAST_NAME - fill_in t('in_person_proofing.form.state_id.address1'), - with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1 - fill_in t('in_person_proofing.form.state_id.address2'), - with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2 - fill_in t('in_person_proofing.form.state_id.city'), - with: InPersonHelper::GOOD_IDENTITY_DOC_CITY - click_idv_continue - - expect(page).to have_current_path(idv_in_person_step_path(step: :address), wait: 10) - end - - it 'shows hints when user selects Puerto Rico as state', - allow_browser_log: true do - sign_in_and_2fa_user - begin_in_person_proofing - complete_prepare_step - complete_location_step - expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) - - # state id page - select 'Puerto Rico', - from: t('in_person_proofing.form.state_id.identity_doc_address_state') - - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) - - # change state selection - fill_out_state_id_form_ok - expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) - expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) - - # re-select puerto rico - select 'Puerto Rico', - from: t('in_person_proofing.form.state_id.identity_doc_address_state') - click_idv_continue - - expect(page).to have_current_path(idv_in_person_step_path(step: :address)) - - # address form - select 'Puerto Rico', - from: t('idv.form.state') - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) - - # change selection - fill_out_address_form_ok - expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) - expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) - - # re-select puerto rico - select 'Puerto Rico', - from: t('idv.form.state') - click_idv_continue - - # ssn page - expect(page).to have_current_path(idv_in_person_ssn_url) - complete_ssn_step - - # verify page - expect(page).to have_current_path(idv_in_person_verify_info_path) - expect(page).to have_text('PR').twice - - # update state ID - click_button t('idv.buttons.change_state_id_label') - - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) - click_button t('forms.buttons.submit.update') - - # update address - click_button t('idv.buttons.change_address_label') - - expect(page).to have_content(t('in_person_proofing.headings.update_address')) - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) - expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) - end - end - context 'same address as id is false', allow_browser_log: true do let(:user) { user_with_2fa } @@ -607,7 +458,7 @@ end end - context 'same address as id is true then update is selected on verify info pg', + context 'same address as id is true', allow_browser_log: true do let(:user) { user_with_2fa } @@ -618,32 +469,26 @@ complete_location_step(user) end - it 'can redo the address page form after it is skipped' do - complete_state_id_step(user, same_address_as_id: true) - # skip address step - complete_ssn_step(user) - # click update address button on the verify page - click_button t('idv.buttons.change_address_label') - expect(page).to have_content(t('in_person_proofing.headings.update_address')) - fill_out_address_form_ok(same_address_as_id: true) - click_button t('forms.buttons.submit.update') - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - end - it 'allows user to update their residential address as different from their state id' do complete_state_id_step(user, same_address_as_id: true) + # skip address step b/c residential address is same as state id address complete_ssn_step(user) - # click "update residential address" + # click update residential address click_button t('idv.buttons.change_address_label') expect(page).to have_content(t('in_person_proofing.headings.update_address')) - # change something in the address + # expect address page to have fields populated with address from state id + expect(page).to have_field( + t('idv.form.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1, + ) + + # change part of the address fill_in t('idv.form.address1'), with: 'new address different from state address1' # click update click_button t('forms.buttons.submit.update') - # back to verify page + # verify page expect(page).to have_current_path(idv_in_person_verify_info_path) expect(page).to have_content(t('headings.verify')) expect(page).to have_text('new address different from state address1').once @@ -659,156 +504,38 @@ end end - context 'Updates are made on state ID page starting from Verify Your Information', - allow_browser_log: true do + context 'Outage alert enabled' do let(:user) { user_with_2fa } - before(:each) do - sign_in_and_2fa_user(user) - begin_in_person_proofing(user) - complete_prepare_step(user) - complete_location_step(user) - end - - it 'does not update their previous selection of "Yes, - I live at the address on my state-issued ID"' do - complete_state_id_step(user, same_address_as_id: true) - # skip address step - complete_ssn_step(user) - # expect to be on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - # click update state ID button on the verify page - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - # change address - fill_in t('in_person_proofing.form.state_id.address1'), with: '' - fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' - click_button t('forms.buttons.submit.update') - # expect to be back on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - expect(page).to have_content(t('headings.verify')) - # expect to see state ID address update on verify twice - expect(page).to have_text('test update address').twice # for state id addr and addr update - # click update state id address - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - # expect "Yes, I live at a different address" is checked" - expect(page).to have_checked_field( - t('in_person_proofing.form.state_id.same_address_as_id_yes'), - visible: false, - ) + before do + allow(IdentityConfig.store).to receive(:in_person_outage_message_enabled).and_return(true) end - it 'does not update their previous selection of "No, I live at a different address"' do - complete_state_id_step(user, same_address_as_id: false) - # expect to be on address page - expect(page).to have_content(t('in_person_proofing.headings.address')) - # complete address step - complete_address_step(user) - complete_ssn_step(user) - # expect to be back on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - # click update state ID button on the verify page - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - # change address - fill_in t('in_person_proofing.form.state_id.address1'), with: '' - fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' - click_button t('forms.buttons.submit.update') - # expect to be back on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - expect(page).to have_content(t('headings.verify')) - # expect to see state ID address update on verify - expect(page).to have_text('test update address').once # only state id address update - # click update state id address - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - expect(page).to have_checked_field( - t('in_person_proofing.form.state_id.same_address_as_id_no'), - visible: false, - ) - end + it 'allows the user to generate a barcode despite outage', allow_browser_log: true do + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) - it 'updates their previous selection from "Yes" TO "No, I live at a different address"' do - complete_state_id_step(user, same_address_as_id: true) - # skip address step - complete_ssn_step(user) - # click update state ID button on the verify page - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - # change address - fill_in t('in_person_proofing.form.state_id.address1'), with: '' - fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' - # change response to No - choose t('in_person_proofing.form.state_id.same_address_as_id_no') - click_button t('forms.buttons.submit.update') - # expect to be on address page - expect(page).to have_content(t('in_person_proofing.headings.address')) - # complete address step - complete_address_step(user) - # expect to be on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - # expect to see state ID address update on verify - expect(page).to have_text('test update address').once # only state id address update - # click update state id address - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - # check that the "No, I live at a different address" is checked" - expect(page).to have_checked_field( - t('in_person_proofing.form.state_id.same_address_as_id_no'), - visible: false, + # alert is visible on prepare page + expect(page).to have_content( + t( + 'idv.failure.exceptions.in_person_outage_error_message.post_cta.body', + app_name: APP_NAME, + ), ) - end + complete_all_in_person_proofing_steps + complete_phone_step(user) + complete_enter_password_step(user) + acknowledge_and_confirm_personal_key - it 'updates their previous selection from "No" TO "Yes, - I live at the address on my state-issued ID"' do - complete_state_id_step(user, same_address_as_id: false) - # expect to be on address page - expect(page).to have_content(t('in_person_proofing.headings.address')) - # complete address step - complete_address_step(user) - complete_ssn_step(user) - # expect to be on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - # click update state ID button on the verify page - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - # change address - fill_in t('in_person_proofing.form.state_id.address1'), with: '' - fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' - # change response to Yes - choose t('in_person_proofing.form.state_id.same_address_as_id_yes') - click_button t('forms.buttons.submit.update') - # expect to be back on verify page - expect(page).to have_content(t('headings.verify')) - expect(page).to have_current_path(idv_in_person_verify_info_path) - # expect to see state ID address update on verify twice - expect(page).to have_text('test update address').twice # for state id addr and addr update - # click update state ID button on the verify page - click_button t('idv.buttons.change_state_id_label') - # expect to be on the state ID page - expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) - expect(page).to have_checked_field( - t('in_person_proofing.form.state_id.same_address_as_id_yes'), - visible: false, + # alert is visible on ready to verify page + expect(page).to have_content( + t('idv.failure.exceptions.in_person_outage_error_message.ready_to_verify.body'), ) + expect(page).to have_current_path(idv_in_person_ready_to_verify_path, wait: 10) end end - context 'when manual address entry is enabled for post office search' do + context 'when full form address entry is enabled for post office search' do let(:user) { user_with_2fa } before do diff --git a/spec/features/idv/pending_profile_password_reset_spec.rb b/spec/features/idv/pending_profile_password_reset_spec.rb index dd2421c7cda..bc42be34212 100644 --- a/spec/features/idv/pending_profile_password_reset_spec.rb +++ b/spec/features/idv/pending_profile_password_reset_spec.rb @@ -3,6 +3,12 @@ RSpec.describe 'Resetting password with a pending profile' do include OidcAuthHelper + let(:sp_name) { 'Test SP' } + before do + allow_any_instance_of(ServiceProviderSession).to receive(:sp_name). + and_return(sp_name) + end + scenario 'while GPO pending requires the user to reproof' do user = create(:user, :with_phone, :with_pending_gpo_profile) @@ -19,7 +25,7 @@ user.password = new_password sign_in_live_with_2fa(user) - expect(page).to have_content(t('doc_auth.headings.welcome')) + expect(page).to have_content t('doc_auth.headings.welcome', sp_name: sp_name) expect(current_path).to eq(idv_welcome_path) expect(user.reload.active_or_pending_profile).to be_nil @@ -43,7 +49,7 @@ user.password = new_password sign_in_live_with_2fa(user) - expect(page).to have_content(t('doc_auth.headings.welcome')) + expect(page).to have_content(t('doc_auth.headings.welcome', sp_name: sp_name)) expect(current_path).to eq(idv_welcome_path) expect(user.reload.active_or_pending_profile).to be_nil @@ -65,7 +71,7 @@ user.password = new_password sign_in_live_with_2fa(user) - expect(page).to have_content(t('doc_auth.headings.welcome')) + expect(page).to have_content(t('doc_auth.headings.welcome', sp_name: sp_name)) expect(current_path).to eq(idv_welcome_path) expect(user.reload.active_or_pending_profile).to be_nil diff --git a/spec/features/idv/steps/in_person/address_spec.rb b/spec/features/idv/steps/in_person/address_spec.rb index b6055711e5e..c98fa252f59 100644 --- a/spec/features/idv/steps/in_person/address_spec.rb +++ b/spec/features/idv/steps/in_person/address_spec.rb @@ -23,6 +23,7 @@ it 'allows the user to cancel and start over', allow_browser_log: true do complete_idv_steps_before_address + expect(page).to have_current_path(idv_in_person_proofing_address_url, wait: 10) expect(page).not_to have_content('forms.buttons.back') click_link t('links.cancel') @@ -55,6 +56,143 @@ expect(page).to have_text(InPersonHelper::GOOD_ADDRESS2) expect(page).to have_text(InPersonHelper::GOOD_CITY) expect(page).to have_text(InPersonHelper::GOOD_ZIPCODE) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT_STATE) + end + end + + context 'updating address page' do + it 'has form fields that are pre-populated', allow_browser_log: true do + user = user_with_2fa + complete_idv_steps_before_address(user) + fill_out_address_form_ok + click_idv_continue + complete_ssn_step(user) + + expect(page).to have_current_path(idv_in_person_verify_info_url, wait: 10) + click_link t('idv.buttons.change_address_label') + + # address page has fields that are pre-populated + expect(page).to have_content(t('in_person_proofing.headings.update_address')) + expect(page).to have_field(t('idv.form.address1'), with: InPersonHelper::GOOD_ADDRESS1) + expect(page).to have_field(t('idv.form.address2'), with: InPersonHelper::GOOD_ADDRESS2) + expect(page).to have_field(t('idv.form.city'), with: InPersonHelper::GOOD_CITY) + expect(page).to have_field(t('idv.form.zipcode'), with: InPersonHelper::GOOD_ZIPCODE) + expect(page).to have_field( + t('idv.form.state'), + with: Idp::Constants::MOCK_IDV_APPLICANT_STATE, + ) + end + end + + context 'Transliterable Validation' do + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled). + and_return(true) + end + + it 'shows validation errors', + allow_browser_log: true do + complete_idv_steps_before_address + + fill_out_address_form_ok(same_address_as_id: false) + fill_in t('idv.form.address1'), with: '#1 $treet' + fill_in t('idv.form.address2'), with: 'Gr@nd Lañe^' + fill_in t('idv.form.city'), with: 'N3w C!ty' + click_idv_continue + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.address.errors.unsupported_chars', + char_list: '$', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.address.errors.unsupported_chars', + char_list: '@, ^', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.address.errors.unsupported_chars', + char_list: '!, 3', + ), + ) + + select InPersonHelper::GOOD_STATE, from: t('idv.form.state') + fill_in t('idv.form.address1'), + with: InPersonHelper::GOOD_ADDRESS1 + fill_in t('idv.form.address2'), + with: InPersonHelper::GOOD_ADDRESS2 + fill_in t('idv.form.city'), + with: InPersonHelper::GOOD_CITY + fill_in t('idv.form.zipcode'), + with: InPersonHelper::GOOD_ZIPCODE + click_idv_continue + + expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10) + end + end + + context 'Validation' do + it 'validates zip code input', allow_browser_log: true do + complete_idv_steps_before_address + + fill_out_address_form_ok + # blank out the zip code field + fill_in t('idv.form.zipcode'), with: '' + # try to enter invalid input into the zip code field + fill_in t('idv.form.zipcode'), with: 'invalid input' + expect(page).to have_field(t('idv.form.zipcode'), with: '') + # enter valid characters, but invalid length + fill_in t('idv.form.zipcode'), with: '123' + click_idv_continue + expect(page).to have_css('.usa-error-message', text: t('idv.errors.pattern_mismatch.zipcode')) + # enter a valid zip and make sure we can continue + fill_in t('idv.form.zipcode'), with: '123456789' + expect(page).to have_field(t('idv.form.zipcode'), with: '12345-6789') + click_idv_continue + expect(page).to have_current_path(idv_in_person_ssn_url) + end + end + + context 'State selection' do + it 'shows address hint when user selects state that has a specific hint', + allow_browser_log: true do + complete_idv_steps_before_address + + # address form + select 'Puerto Rico', + from: t('idv.form.state') + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + + # change selection + fill_out_address_form_ok(same_address_as_id: false) + expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + + # re-select puerto rico + select 'Puerto Rico', + from: t('idv.form.state') + click_idv_continue + + # ssn page + expect(page).to have_current_path(idv_in_person_ssn_url) + complete_ssn_step + + # verify page + expect(page).to have_current_path(idv_in_person_verify_info_path, wait: 10) + expect(page).to have_text('PR') + + # update address + click_link t('idv.buttons.change_address_label') + + expect(page).to have_content(t('in_person_proofing.headings.update_address')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) end end end diff --git a/spec/features/idv/steps/in_person/state_id_step_spec.rb b/spec/features/idv/steps/in_person/state_id_step_spec.rb index aa774a0477a..2b761fba616 100644 --- a/spec/features/idv/steps/in_person/state_id_step_spec.rb +++ b/spec/features/idv/steps/in_person/state_id_step_spec.rb @@ -8,28 +8,454 @@ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end - it 'validates zip code input', allow_browser_log: true do - user = user_with_2fa - - sign_in_and_2fa_user(user) - begin_in_person_proofing(user) - complete_prepare_step(user) - complete_location_step(user) - expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) - fill_out_state_id_form_ok(same_address_as_id: true) - # blank out the zip code field - fill_in t('in_person_proofing.form.state_id.zipcode'), with: '' - # try to enter invalid input into the zip code field - fill_in t('in_person_proofing.form.state_id.zipcode'), with: 'invalid input' - expect(page).to have_field(t('in_person_proofing.form.state_id.zipcode'), with: '') - # enter valid characters, but invalid length - fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123' - click_idv_continue - expect(page).to have_css('.usa-error-message', text: t('idv.errors.pattern_mismatch.zipcode')) - # enter a valid zip and make sure we can continue - fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123456789' - expect(page).to have_field(t('in_person_proofing.form.state_id.zipcode'), with: '12345-6789') - click_idv_continue - expect(page).to have_current_path(idv_in_person_ssn_url) + context 'when visiting state id for the first time' do + it 'displays correct heading and button text', allow_browser_log: true do + complete_steps_before_state_id_step + + expect(page).to have_content(t('forms.buttons.continue')) + expect(page).to have_content( + t( + 'in_person_proofing.headings.state_id_milestone_2', + ).tr(' ', ' '), + ) + end + + it 'allows the user to cancel and start over', allow_browser_log: true do + complete_steps_before_state_id_step + + expect(page).not_to have_content('forms.buttons.back') + + click_link t('links.cancel') + click_on t('idv.cancel.actions.start_over') + expect(page).to have_current_path(idv_welcome_path) + end + + it 'allows the user to cancel and return', allow_browser_log: true do + complete_steps_before_state_id_step + + expect(page).not_to have_content('forms.buttons.back') + + click_link t('links.cancel') + click_on t('idv.cancel.actions.keep_going') + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + end + + it 'allows user to submit valid inputs on form', allow_browser_log: true do + complete_steps_before_state_id_step + fill_out_state_id_form_ok(same_address_as_id: true) + click_idv_continue + + expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10) + complete_ssn_step + + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + expect(page).to have_current_path(idv_in_person_verify_info_url) + expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME) + expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME) + expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT) + expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE) + end + end + + context 'updating state id page' do + it 'has form fields that are pre-populated', allow_browser_log: true do + complete_steps_before_state_id_step + + fill_out_state_id_form_ok(same_address_as_id: true) + click_idv_continue + expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10) + complete_ssn_step + expect(page).to have_current_path(idv_in_person_verify_info_url, wait: 10) + click_button t('idv.buttons.change_state_id_label') + + # state id page has fields that are pre-populated + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_field( + t('in_person_proofing.form.state_id.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.last_name'), + with: InPersonHelper::GOOD_LAST_NAME, + ) + expect(page).to have_field(t('components.memorable_date.month'), with: '10') + expect(page).to have_field(t('components.memorable_date.day'), with: '6') + expect(page).to have_field(t('components.memorable_date.year'), with: '1938') + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_jurisdiction'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_number'), + with: InPersonHelper::GOOD_STATE_ID_NUMBER, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address2'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.city'), + with: InPersonHelper::GOOD_IDENTITY_DOC_CITY, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.zipcode'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.identity_doc_address_state'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + + context 'same address as id', + allow_browser_log: true do + let(:user) { user_with_2fa } + + before(:each) do + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) + complete_prepare_step(user) + complete_location_step(user) + end + + it 'does not update their previous selection of "Yes, + I live at the address on my state-issued ID"' do + complete_state_id_step(user, same_address_as_id: true) + # skip address step + complete_ssn_step(user) + # expect to be on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + click_button t('forms.buttons.submit.update') + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_content(t('headings.verify')) + # expect to see state ID address update on verify twice + expect(page).to have_text('test update address').twice # for state id addr and addr update + # click update state id address + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # expect "Yes, I live at a different address" is checked" + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + + it 'does not update their previous selection of "No, I live at a different address"' do + complete_state_id_step(user, same_address_as_id: false) + # expect to be on address page + expect(page).to have_content(t('in_person_proofing.headings.address')) + # complete address step + complete_address_step(user) + complete_ssn_step(user) + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + click_button t('forms.buttons.submit.update') + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_content(t('headings.verify')) + # expect to see state ID address update on verify + expect(page).to have_text('test update address').once # only state id address update + # click update state id address + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_no'), + visible: false, + ) + end + + it 'updates their previous selection from "Yes" TO "No, I live at a different address"' do + complete_state_id_step(user, same_address_as_id: true) + # skip address step + complete_ssn_step(user) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + # change response to No + choose t('in_person_proofing.form.state_id.same_address_as_id_no') + click_button t('forms.buttons.submit.update') + # expect to be on address page + expect(page).to have_content(t('in_person_proofing.headings.address')) + # complete address step + complete_address_step(user) + # expect to be on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # expect to see state ID address update on verify + expect(page).to have_text('test update address').once # only state id address update + # click update state id address + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # check that the "No, I live at a different address" is checked" + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_no'), + visible: false, + ) + end + + it 'updates their previous selection from "No" TO "Yes, + I live at the address on my state-issued ID"' do + complete_state_id_step(user, same_address_as_id: false) + # expect to be on address page + expect(page).to have_content(t('in_person_proofing.headings.address')) + # complete address step + complete_address_step(user) + complete_ssn_step(user) + # expect to be on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + # change response to Yes + choose t('in_person_proofing.form.state_id.same_address_as_id_yes') + click_button t('forms.buttons.submit.update') + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # expect to see state ID address update on verify twice + expect(page).to have_text('test update address').twice # for state id addr and addr update + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + end + end + + context 'Validation' do + it 'validates zip code input', allow_browser_log: true do + complete_steps_before_state_id_step + + fill_out_state_id_form_ok(same_address_as_id: true) + fill_in t('in_person_proofing.form.state_id.zipcode'), with: '' + fill_in t('in_person_proofing.form.state_id.zipcode'), with: 'invalid input' + expect(page).to have_field(t('in_person_proofing.form.state_id.zipcode'), with: '') + + # enter valid characters, but invalid length + fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123' + click_idv_continue + expect(page).to have_css('.usa-error-message', text: t('idv.errors.pattern_mismatch.zipcode')) + + # enter a valid zip and make sure we can continue + fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123456789' + expect(page).to have_field(t('in_person_proofing.form.state_id.zipcode'), with: '12345-6789') + click_idv_continue + expect(page).to have_current_path(idv_in_person_ssn_url) + end + + it 'shows error for dob under minimum age', allow_browser_log: true do + complete_steps_before_state_id_step + + fill_in t('components.memorable_date.month'), with: '1' + fill_in t('components.memorable_date.day'), with: '1' + fill_in t('components.memorable_date.year'), with: Time.zone.now.strftime('%Y') + click_idv_continue + expect(page).to have_content( + t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + ) + + year = (Time.zone.now - 13.years).strftime('%Y') + fill_in t('components.memorable_date.year'), with: year + click_idv_continue + expect(page).not_to have_content( + t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + ) + end + end + + context 'Transliterable Validation' do + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled). + and_return(true) + end + + it 'shows validation errors', + allow_browser_log: true do + complete_steps_before_state_id_step + + fill_out_state_id_form_ok + fill_in t('in_person_proofing.form.state_id.first_name'), with: 'T0mmy "Lee"' + fill_in t('in_person_proofing.form.state_id.last_name'), with: 'Джейкоб' + fill_in t('in_person_proofing.form.state_id.address1'), with: '#1 $treet' + fill_in t('in_person_proofing.form.state_id.address2'), with: 'Gr@nd Lañe^' + fill_in t('in_person_proofing.form.state_id.city'), with: 'N3w C!ty' + click_idv_continue + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '", 0', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: 'Д, б, е, ж, й, к, о', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '$', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '@, ^', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '!, 3', + ), + ) + + fill_in t('in_person_proofing.form.state_id.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME + fill_in t('in_person_proofing.form.state_id.last_name'), + with: InPersonHelper::GOOD_LAST_NAME + fill_in t('in_person_proofing.form.state_id.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1 + fill_in t('in_person_proofing.form.state_id.address2'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2 + fill_in t('in_person_proofing.form.state_id.city'), + with: InPersonHelper::GOOD_IDENTITY_DOC_CITY + click_idv_continue + + expect(page).to have_current_path(idv_in_person_step_path(step: :address), wait: 10) + end + end + + context 'State selection' do + it 'shows address hint when user selects state that has a specific hint', + allow_browser_log: true do + complete_steps_before_state_id_step + + # state id page + select 'Puerto Rico', + from: t('in_person_proofing.form.state_id.identity_doc_address_state') + + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + + # change state selection + fill_out_state_id_form_ok(same_address_as_id: true) + expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + + # re-select puerto rico + select 'Puerto Rico', + from: t('in_person_proofing.form.state_id.identity_doc_address_state') + click_idv_continue + + # ssn page + expect(page).to have_current_path(idv_in_person_ssn_url) + complete_ssn_step + + # verify page + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_text('PR') + + # update state ID + click_button t('idv.buttons.change_state_id_label') + + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + end + + it 'shows id number hint when user selects issuing state that has a specific hint', + allow_browser_log: true do + complete_steps_before_state_id_step + + # expect default hint to be present + expect(page).to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + + select 'Texas', + from: t('in_person_proofing.form.state_id.state_id_jurisdiction') + expect(page).to have_content(t('in_person_proofing.form.state_id.state_id_number_texas_hint')) + expect(page).not_to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + + select 'Florida', + from: t('in_person_proofing.form.state_id.state_id_jurisdiction') + expect(page).not_to have_content( + t('in_person_proofing.form.state_id.state_id_number_texas_hint'), + ) + expect(page).not_to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + expect(page).to have_content( + t('in_person_proofing.form.state_id.state_id_number_florida_hint'), + ) + + # select a state without a state specific hint + select 'Ohio', + from: t('in_person_proofing.form.state_id.state_id_jurisdiction') + expect(page).to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + expect(page).not_to have_content( + t('in_person_proofing.form.state_id.state_id_number_texas_hint'), + ) + expect(page).not_to have_content( + t('in_person_proofing.form.state_id.state_id_number_florida_hint'), + ) + end end end diff --git a/spec/features/remember_device/totp_spec.rb b/spec/features/remember_device/totp_spec.rb index 207b6fd6d62..f3de6d554bc 100644 --- a/spec/features/remember_device/totp_spec.rb +++ b/spec/features/remember_device/totp_spec.rb @@ -41,17 +41,29 @@ def remember_device_and_sign_out_user context 'update totp' do def remember_device_and_sign_out_user + auth_app_config = create(:auth_app_configuration, user:) + name = auth_app_config.name + sign_in_and_2fa_user(user) visit account_two_factor_authentication_path - page.find('.remove-auth-app').click # Delete - click_on t('account.index.totp_confirm_delete') + + click_link( + format( + '%s: %s', + t('two_factor_authentication.auth_app.manage_accessible_label'), + name, + ), + ) + + click_button t('two_factor_authentication.auth_app.delete') + travel_to(10.seconds.from_now) # Travel past the revoked at date from disabling the device click_link t('account.index.auth_app_add'), href: authenticator_setup_url fill_in_totp_name fill_in :code, with: totp_secret_from_page check t('forms.messages.remember_device') click_submit_default - expect(page).to have_current_path(account_two_factor_authentication_path) + expect(page).to have_current_path(account_path) first(:button, t('links.sign_out')).click user end diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index 4f297b4941a..56a86cf4b21 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -587,7 +587,6 @@ def attempt_to_bypass_2fa context 'sign in' do it 'allows user to be signed in without issue' do mock_webauthn_verification_challenge - sign_in_user(webauthn_configuration.user) mock_successful_webauthn_authentication { click_webauthn_authenticate_button } diff --git a/spec/features/users/totp_management_spec.rb b/spec/features/users/totp_management_spec.rb index de496c4e0c5..49b43395fb2 100644 --- a/spec/features/users/totp_management_spec.rb +++ b/spec/features/users/totp_management_spec.rb @@ -4,18 +4,95 @@ context 'when the user has totp enabled' do let(:user) { create(:user, :fully_registered, :with_authentication_app) } - it 'allows the user to disable their totp app' do + it 'allows user to delete a platform authenticator when another 2FA option is set up' do + auth_app_config = create(:auth_app_configuration, user:) + name = auth_app_config.name + + sign_in_and_2fa_user(user) + visit account_two_factor_authentication_path + + expect(user.reload.auth_app_configurations.count).to eq(2) + expect(page).to have_content(name) + + click_link( + format( + '%s: %s', + t('two_factor_authentication.auth_app.manage_accessible_label'), + name, + ), + ) + + expect(current_path).to eq(edit_auth_app_path(id: auth_app_config.id)) + + click_button t('two_factor_authentication.auth_app.delete') + + expect(page).to have_content(t('two_factor_authentication.auth_app.deleted')) + expect(user.reload.auth_app_configurations.count).to eq(1) + end + + it 'allows user to rename an authentication app app' do + auth_app_configuration = create(:auth_app_configuration, user:) + name = auth_app_configuration.name + sign_in_and_2fa_user(user) visit account_two_factor_authentication_path - expect(page).to have_content(t('two_factor_authentication.login_options.auth_app')) - expect(page.find('.remove-auth-app')).to_not be_nil - page.find('.remove-auth-app').click + expect(page).to have_content(name) + + click_link( + format( + '%s: %s', + t('two_factor_authentication.auth_app.manage_accessible_label'), + name, + ), + ) + + expect(current_path).to eq(edit_auth_app_path(id: auth_app_configuration.id)) + expect(page).to have_field( + t('two_factor_authentication.auth_app.nickname'), + with: name, + ) + + fill_in t('two_factor_authentication.auth_app.nickname'), with: 'new name' + + click_button t('two_factor_authentication.auth_app.change_nickname') + + expect(page).to have_content('new name') + expect(page).to have_content(t('two_factor_authentication.auth_app.renamed')) + end + + it 'requires a user to use a unique name when renaming' do + existing_auth_app_configuration = create(:auth_app_configuration, user:, name: 'existing') + new_app_auth_configuration = create(:auth_app_configuration, user:, name: 'new existing') + name = existing_auth_app_configuration.name + + sign_in_and_2fa_user(user) + visit account_two_factor_authentication_path + + expect(page).to have_content(name) + + click_link( + format( + '%s: %s', + t('two_factor_authentication.auth_app.manage_accessible_label'), + name, + ), + ) + + expect(current_path).to eq(edit_auth_app_path(id: existing_auth_app_configuration.id)) + expect(page).to have_field( + t('two_factor_authentication.auth_app.nickname'), + with: name, + ) + + fill_in t('two_factor_authentication.auth_app.nickname'), + with: new_app_auth_configuration.name + + click_button t('two_factor_authentication.auth_app.change_nickname') - expect(current_path).to eq auth_app_delete_path - click_on t('account.index.totp_confirm_delete') + expect(current_path).to eq(edit_auth_app_path(id: existing_auth_app_configuration.id)) - expect(current_path).to eq account_two_factor_authentication_path + expect(page).to have_content(t('errors.manage_authenticator.unique_name_error')) end end diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 7567ee11224..7b1f67bae47 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -83,11 +83,27 @@ let(:user) { create(:user, :fully_registered, :with_webauthn_platform) } context 'with javascript enabled', :js do - it 'displays the authenticator option' do - sign_in_user(user) - click_on t('two_factor_authentication.login_options_link_text') + context ' with device that supports authenticator' do + it 'displays the authenticator option' do + sign_in_user(user) + click_on t('two_factor_authentication.login_options_link_text') - expect(webauthn_option_hidden?).to eq(false) + expect(webauthn_option_hidden?).to eq(false) + end + end + + context 'with device that doesnt support authenticator' do + it 'redirects to options page on sign in and shows the option' do + email ||= user.email_addresses.first.email + password = user.password + allow(UserMailer).to receive(:new_device_sign_in).and_call_original + visit new_user_session_path + set_hidden_field('platform_authenticator_available', 'false') + fill_in_credentials_and_submit(email, password) + continue_as(email, password) + expect(current_path).to eq(login_two_factor_options_path) + expect(webauthn_option_hidden?).to eq(false) + end end end diff --git a/spec/features/webauthn/sign_in_spec.rb b/spec/features/webauthn/sign_in_spec.rb index bd9440c192e..69f86e4c0fc 100644 --- a/spec/features/webauthn/sign_in_spec.rb +++ b/spec/features/webauthn/sign_in_spec.rb @@ -75,9 +75,33 @@ mock_webauthn_verification_challenge sign_in_user(user) + expect(current_url).to eq(login_two_factor_webauthn_url(platform: true)) mock_cancelled_webauthn_authentication { click_webauthn_authenticate_button } expect(page).to have_content(t('two_factor_authentication.webauthn_platform_header_text')) end + + context 'with device that doesnt support authenticator' do + before do + email ||= user.email_addresses.first.email + password = user.password + allow(UserMailer).to receive(:new_device_sign_in).and_call_original + visit new_user_session_path + set_hidden_field('platform_authenticator_available', 'false') + fill_in_credentials_and_submit(email, password) + continue_as(email, password) + end + + it 'redirects to options page on sign in' do + expect(current_path).to eq(login_two_factor_options_path) + end + + it 'allows user to go to options page and still select webauthn as their option' do + expect(current_path).to eq(login_two_factor_options_path) + select_2fa_option('webauthn_platform', visible: :all) + click_continue + expect(current_url).to eq(login_two_factor_webauthn_url(platform: true)) + end + end end end diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_with_face_match_fail.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_with_face_match_fail.json new file mode 100644 index 00000000000..bba5ab5d30c --- /dev/null +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_with_face_match_fail.json @@ -0,0 +1,1100 @@ +{ + "Status": { + "ConversationId": "70000300394121", + "RequestId": "614507871", + "TransactionStatus": "passed", + "TransactionReasonCode": { + "Code": "trueid_pass", + "Description": "TRUEID PASS" + }, + "Reference": "ca6e36c4-8a55-4831-aa8a-38d78b7c80e3" + }, + "Products": [ + { + "ProductType": "TrueID", + "ExecutedStepName": "True_ID_Step", + "ProductConfigurationName": "GSA2.V3.TrueID.CROP.PT.test", + "ProductStatus": "pass", + "ParameterDetails": [ + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocumentName", + "Values": [{ "Value": "Maryland (MD) Driver's License - STAR" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthResult", + "Values": [{ "Value": "Passed" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthTamperResult", + "Values": [{ "Value": "Passed" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthTamperSensitivity", + "Values": [{ "Value": "Normal" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerCode", + "Values": [{ "Value": "MD" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerName", + "Values": [{ "Value": "Maryland" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerType", + "Values": [{ "Value": "StateProvince" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassCode", + "Values": [{ "Value": "DriversLicense" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClass", + "Values": [{ "Value": "DriversLicense" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassName", + "Values": [{ "Value": "Drivers License" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIsGeneric", + "Values": [{ "Value": "false" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssue", + "Values": [{ "Value": "2016" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssueType", + "Values": [{ "Value": "Driver's License - STAR" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocSize", + "Values": [{ "Value": "ID1" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ClassificationMode", + "Values": [{ "Value": "Automatic" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "OrientationChanged", + "Values": [{ "Value": "true" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "PresentationChanged", + "Values": [{ "Value": "false" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Side", + "Values": [{ "Value": "Front" }, { "Value": "Back" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "GlareMetric", + "Values": [{ "Value": "100" }, { "Value": "100" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "SharpnessMetric", + "Values": [{ "Value": "65" }, { "Value": "65" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsTampered", + "Values": [{ "Value": "0" }, { "Value": "0" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsCropped", + "Values": [{ "Value": "1" }, { "Value": "1" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "HorizontalResolution", + "Values": [{ "Value": "600" }, { "Value": "600" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "VerticalResolution", + "Values": [{ "Value": "600" }, { "Value": "600" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Light", + "Values": [{ "Value": "White" }, { "Value": "White" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "MimeType", + "Values": [ + { "Value": "image/vnd.ms-photo" }, + { "Value": "image/vnd.ms-photo" } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "ImageMetrics_Id", + "Values": [ + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "FullName", + "Values": [{ "Value": "DAVID LICENSE SAMPLE" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Sex", + "Values": [{ "Value": "Male" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Age", + "Values": [{ "Value": "33" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Year", + "Values": [{ "Value": "1985" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Month", + "Values": [{ "Value": "7" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Day", + "Values": [{ "Value": "1" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Year", + "Values": [{ "Value": "2099" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Month", + "Values": [{ "Value": "10" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Day", + "Values": [{ "Value": "15" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AlertName", + "Values": [{ "Value": "Document Expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AlertName", + "Values": [{ "Value": "2D Barcode Content" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AlertName", + "Values": [{ "Value": "2D Barcode Read" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AlertName", + "Values": [{ "Value": "Barcode Encoding" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AlertName", + "Values": [{ "Value": "Birth Date Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AlertName", + "Values": [{ "Value": "Birth Date Valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AlertName", + "Values": [{ "Value": "Document Classification" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_10_AlertName", + "Values": [{ "Value": "Document Crosscheck Aggregation" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_11_AlertName", + "Values": [{ "Value": "Document Number Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_15_AlertName", + "Values": [{ "Value": "Expiration Date Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_16_AlertName", + "Values": [{ "Value": "Expiration Date Valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_17_AlertName", + "Values": [{ "Value": "Full Name Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_18_AlertName", + "Values": [{ "Value": "Issue Date Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_19_AlertName", + "Values": [{ "Value": "Issue Date Valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_20_AlertName", + "Values": [{ "Value": "Series Expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_21_AlertName", + "Values": [{ "Value": "Sex Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Model", + "Values": [{ "Value": "Text Tampering Detection V1.3.1 (Beta)" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Model", + "Values": [{ "Value": "Photo Tampering Detection V2.4" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Model", + "Values": [{ "Value": "Text Tampering Detection V1.2.1" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_Model", + "Values": [{ "Value": "Physical Document Presence V2.5" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Checked if the document is expired." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Checked the contents of the two-dimensional barcode on the document." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AuthenticationResult", + "Values": [ + { + "Value": "Attention", + "Detail": "Verified that the two-dimensional barcode on the document was read successfully." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the format of the barcode." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable birth date field to the human-readable birth date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the birth date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the type of document is supported and is able to be fully authenticated." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_10_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compared the machine-readable fields to the human-readable fields." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_11_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable document number field to the human-readable document number field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_15_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable expiration date field to the human-readable expiration date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_16_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the expiration date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_17_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable full name field to the human-readable full name field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_18_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable issue date field to the human-readable issue date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_19_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the issue date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_20_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified whether the document type is still in circulation." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_21_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable sex field to the human-readable sex field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_Disposition", + "Values": [{ "Value": "The document has expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Disposition", + "Values": [{ "Value": "A visible pattern was not found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Disposition", + "Values": [ + { + "Value": "Evidence suggests that the document may have been tampered with." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_Disposition", + "Values": [{ "Value": "The 2D barcode is formatted correctly" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_Disposition", + "Values": [{ "Value": "The 2D barcode was read successfully" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_Disposition", + "Values": [ + { + "Value": "The barcode encoding is consistent with the expected encoding for the type" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_Disposition", + "Values": [{ "Value": "The birth dates match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_Disposition", + "Values": [{ "Value": "The birth date is valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_Disposition", + "Values": [{ "Value": "The document type is supported" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_10_Disposition", + "Values": [ + { + "Value": "There are not a large number of differences between electronic and human-readable data sources" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_11_Disposition", + "Values": [{ "Value": "The document numbers match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Disposition", + "Values": [ + { "Value": "No evidence of document tampering was detected." } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Disposition", + "Values": [ + { "Value": "No evidence of document tampering was detected." } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_Disposition", + "Values": [ + { "Value": "No evidence of document tampering was detected." } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_15_Disposition", + "Values": [{ "Value": "The expiration dates match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_16_Disposition", + "Values": [{ "Value": "The expiration date is valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_17_Disposition", + "Values": [{ "Value": "The full names match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_18_Disposition", + "Values": [{ "Value": "The issue dates match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_19_Disposition", + "Values": [{ "Value": "The issue date is valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_20_Disposition", + "Values": [{ "Value": "The series has not expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_21_Disposition", + "Values": [{ "Value": "The sexes match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_Disposition", + "Values": [{ "Value": "A visible pattern was found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_Disposition", + "Values": [{ "Value": "A visible pattern was found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_Disposition", + "Values": [{ "Value": "A visible pattern was found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Regions", + "Values": [{ "Value": "Background" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Regions", + "Values": [ + { "Value": "Address" }, + { "Value": "Birth Date" }, + { "Value": "Document Number" }, + { "Value": "Expiration Date" }, + { "Value": "Full Name" }, + { "Value": "Issue Date" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Regions", + "Values": [{ "Value": "Photo" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Regions", + "Values": [ + { "Value": "Address" }, + { "Value": "Birth Date" }, + { "Value": "Document Number" }, + { "Value": "Expiration Date" }, + { "Value": "Full Name" }, + { "Value": "Issue Date" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_Regions", + "Values": [{ "Value": "Expires Label" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_Regions", + "Values": [{ "Value": "USA" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_Regions", + "Values": [{ "Value": "Background Upper" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Regions_Reference", + "Values": [{ "Value": "faacfb79-d0a1-4a8e-b868-20c604988e84" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Regions_Reference", + "Values": [ + { "Value": "c8be94b6-78ac-4e85-88cb-e17880371e4a" }, + { "Value": "a8226d92-e62c-42a3-a206-ab7c3e3d9796" }, + { "Value": "c2e18c41-e3de-46a8-abc7-3412015a6cef" }, + { "Value": "80f8f290-daa0-47e2-828e-52106bb26f31" }, + { "Value": "2c74b850-dd89-41bb-a21c-70ae0563ef77" }, + { "Value": "63bf5053-f81f-493f-aff0-33c07d07a894" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Regions_Reference", + "Values": [{ "Value": "f29b1fe5-6482-4b39-8b4a-d91caf4ecb57" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Regions_Reference", + "Values": [ + { "Value": "c8be94b6-78ac-4e85-88cb-e17880371e4a" }, + { "Value": "a8226d92-e62c-42a3-a206-ab7c3e3d9796" }, + { "Value": "c2e18c41-e3de-46a8-abc7-3412015a6cef" }, + { "Value": "80f8f290-daa0-47e2-828e-52106bb26f31" }, + { "Value": "2c74b850-dd89-41bb-a21c-70ae0563ef77" }, + { "Value": "63bf5053-f81f-493f-aff0-33c07d07a894" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_Regions_Reference", + "Values": [{ "Value": "d55f2c66-f84f-4213-a660-4ff9e5d0fde5" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_Regions_Reference", + "Values": [{ "Value": "bbf6ba02-ee3f-4b5c-a5d0-2fdb39ac79f7" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_Regions_Reference", + "Values": [{ "Value": "20203cb8-f8a4-4a5b-999f-3700f73fe4fe" }] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchResult", + "Values": [{"Value": "Fail"}] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchScore", + "Values": [{"Value": "50"}] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceStatusCode", + "Values": [{"Value": "1"}] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceErrorMessage", + "Values": [{"Value": "Successful. Liveness: Live"}] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FullName", + "Values": [{ "Value": "DAVID LICENSE SAMPLE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Surname", + "Values": [{ "Value": "SAMPLE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_GivenName", + "Values": [{ "Value": "DAVID LICENSE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FirstName", + "Values": [{ "Value": "DAVID" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_MiddleName", + "Values": [{ "Value": "LICENSE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Year", + "Values": [{ "Value": "1986" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Month", + "Values": [{ "Value": "7" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Day", + "Values": [{ "Value": "1" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentClassName", + "Values": [{ "Value": "Drivers License" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentNumber", + "Values": [{ "Value": "M555555555555" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Year", + "Values": [{ "Value": "2099" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Month", + "Values": [{ "Value": "10" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_xpirationDate_Day", + "Values": [{ "Value": "15" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateCode", + "Values": [{ "Value": "MD" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateName", + "Values": [{ "Value": "Maryland" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_CountryCode", + "Values": [{ "Value": "USA" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Address", + "Values": [ + { + "Value": "123 ABC AVExE2x80xA8ANYTOWN, MD 12345" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine1", + "Values": [{ "Value": "123 ABC AVE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine2", + "Values": [{ "Value": "APT 3E" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_City", + "Values": [{ "Value": "ANYTOWN" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_State", + "Values": [{ "Value": "MD" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_PostalCode", + "Values": [{ "Value": "12345" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Height", + "Values": [{ "Value": "5' 9\"" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Year", + "Values": [{ "Value": "2016" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Month", + "Values": [{ "Value": "10" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Day", + "Values": [{ "Value": "15" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseClass", + "Values": [{ "Value": "C" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseRestrictions", + "Values": [{ "Value": "B" }] + }, + { + "Group": "DOCUMENT_REGION", + "Name": "DocumentRegion_Id", + "Values": [ + { "Value": "ce2cf0e2-5373-4ec2-84e8-7fe44a01642b" }, + { "Value": "0b4f4f2b-cbd6-43e9-ac67-55bf2bdd9df5" }, + { "Value": "c8be94b6-78ac-4e85-88cb-e17880371e4a" }, + { "Value": "a0fdf00c-071c-4d8e-81af-8af0fc7688b8" }, + { "Value": "faacfb79-d0a1-4a8e-b868-20c604988e84" }, + { "Value": "3362ad4b-a36b-487e-826c-c748c7b04e8d" }, + { "Value": "20203cb8-f8a4-4a5b-999f-3700f73fe4fe" }, + { "Value": "a8226d92-e62c-42a3-a206-ab7c3e3d9796" }, + { "Value": "a3e3a625-8b0e-4deb-a91e-86afc55d036b" }, + { "Value": "cda4092d-9208-4871-bbc1-1b732c299d26" }, + { "Value": "42770baf-3a4a-4477-9e5f-4400237273fe" }, + { "Value": "c2e18c41-e3de-46a8-abc7-3412015a6cef" }, + { "Value": "b36594f2-d19c-48aa-98c2-2b4f8429744f" }, + { "Value": "80f8f290-daa0-47e2-828e-52106bb26f31" }, + { "Value": "d55f2c66-f84f-4213-a660-4ff9e5d0fde5" }, + { "Value": "26ca0c85-01ab-4311-bfd5-d27d2ee975eb" }, + { "Value": "0af79f76-1542-4391-ad17-a5d6169be57f" }, + { "Value": "2c74b850-dd89-41bb-a21c-70ae0563ef77" }, + { "Value": "64e8ee97-2b24-452d-a5af-1627951aa737" }, + { "Value": "63bf5053-f81f-493f-aff0-33c07d07a894" }, + { "Value": "f29b1fe5-6482-4b39-8b4a-d91caf4ecb57" }, + { "Value": "35442fb1-c3bb-4f2f-ad9e-537a8f0e7e0f" }, + { "Value": "29964031-e072-4204-a9ae-b2b7122bfdc1" }, + { "Value": "a3bbdf8b-62f5-438f-bf72-091bb2f6f0ff" }, + { "Value": "42d0c49e-fc7a-45da-8f6d-8896c4b5267f" }, + { "Value": "5430efcb-523b-4c62-a190-e9aa7eea4ebd" }, + { "Value": "bbf6ba02-ee3f-4b5c-a5d0-2fdb39ac79f7" }, + { "Value": "0686341c-0b3f-4544-840e-58822120ef06" } + ] + }, + { + "Group": "DOCUMENT_REGION", + "Name": "DocumentRegion_Key", + "Values": [ + { "Value": "1D Barcode" }, + { "Value": "2D Barcode" }, + { "Value": "Address" }, + { "Value": "Alaska Validator" }, + { "Value": "Background" }, + { "Value": "Background Lower" }, + { "Value": "Background Upper" }, + { "Value": "Birth Date" }, + { "Value": "Birth Date" }, + { "Value": "DOB Label" }, + { "Value": "DOB Label Text" }, + { "Value": "Document Number" }, + { "Value": "Document Type" }, + { "Value": "Expiration Date" }, + { "Value": "Expires Label" }, + { "Value": "Expires Label Position" }, + { "Value": "Eye Color" }, + { "Value": "Full Name" }, + { "Value": "Height" }, + { "Value": "Issue Date" }, + { "Value": "Photo" }, + { "Value": "Photo Printing" }, + { "Value": "Secondary Photo" }, + { "Value": "Sex" }, + { "Value": "Sex Height Labels" }, + { "Value": "Signature" }, + { "Value": "USA" }, + { "Value": "Weight" } + ] + }, + { + "Group": "DOCUMENT_REGION", + "Name": "DocumentRegion_ImageReference", + "Values": [ + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" } + ] + } + ] + }, + { + "ProductType": "TrueID_Decision", + "ExecutedStepName": "Decision", + "ProductConfigurationName": "TRUEID_PASS", + "ProductStatus": "pass", + "ProductReason": { + "Code": "trueid_pass", + "Description": "TRUEID PASS" + } + } + ] +} diff --git a/spec/forms/two_factor_authentication/auth_app_delete_form_spec.rb b/spec/forms/two_factor_authentication/auth_app_delete_form_spec.rb new file mode 100644 index 00000000000..3e8c3f15dcb --- /dev/null +++ b/spec/forms/two_factor_authentication/auth_app_delete_form_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' + +RSpec.describe TwoFactorAuthentication::AuthAppDeleteForm do + let(:user) { create(:user) } + let(:configuration) { create(:auth_app_configuration, user:) } + let(:configuration_id) { configuration&.id } + let(:form) { described_class.new(user:, configuration_id:) } + + describe '#submit' do + let(:result) { form.submit } + + context 'with having another mfa enabled' do + let(:user) { create(:user, :with_phone) } + + it 'returns a successful result' do + expect(result.success?).to eq(true) + expect(result.to_h).to eq(success: true, configuration_id:) + end + + context 'with blank configuration' do + let(:configuration) { nil } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { configuration_not_found: true }, + }, + configuration_id:, + ) + end + end + + context 'with configuration that does not exist' do + let(:configuration_id) { 'does-not-exist' } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { configuration_not_found: true }, + }, + configuration_id:, + ) + end + end + + context 'with configuration not belonging to the user' do + let(:configuration) { create(:auth_app_configuration) } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { configuration_not_found: true }, + }, + configuration_id:, + ) + end + end + end + + context 'with user not having another mfa enabled' do + let(:user) { create(:user) } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { only_method: true }, + }, + configuration_id:, + ) + end + end + end + + describe '#configuration' do + subject(:form_configuration) { form.configuration } + + it 'returns configuration' do + expect(form_configuration).to eq(configuration) + end + end +end diff --git a/spec/forms/two_factor_authentication/auth_app_update_form_spec.rb b/spec/forms/two_factor_authentication/auth_app_update_form_spec.rb new file mode 100644 index 00000000000..87d1dd46948 --- /dev/null +++ b/spec/forms/two_factor_authentication/auth_app_update_form_spec.rb @@ -0,0 +1,142 @@ +require 'rails_helper' + +RSpec.describe TwoFactorAuthentication::AuthAppUpdateForm do + let(:user) { create(:user) } + let(:original_name) { 'original-name' } + let(:configuration) { create(:auth_app_configuration, user:, name: original_name) } + let(:configuration_id) { configuration&.id } + let(:form) { described_class.new(user:, configuration_id:) } + + describe '#submit' do + let(:name) { 'new-namae' } + let(:result) { form.submit(name:) } + + it 'returns a successful result' do + expect(result.success?).to eq(true) + expect(result.to_h).to eq(success: true, configuration_id:) + end + + it 'saves the new name' do + result + + expect(configuration.reload.name).to eq(name) + end + + context 'with blank configuration' do + let(:configuration) { nil } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { configuration_not_found: true }, + }, + configuration_id:, + ) + end + end + + context 'with configuration that does not exist' do + let(:configuration_id) { 'does-not-exist' } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { configuration_not_found: true }, + }, + configuration_id:, + ) + end + end + + context 'with configuration not belonging to the user' do + let(:configuration) { create(:auth_app_configuration, name: original_name) } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + configuration_id: { configuration_not_found: true }, + }, + configuration_id:, + ) + end + + it 'does not save the new name' do + expect(configuration).not_to receive(:save) + + result + + expect(configuration.reload.name).to eq(original_name) + end + end + + context 'with blank name' do + let(:name) { '' } + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + name: { blank: true }, + }, + configuration_id:, + ) + end + + it 'does not save the new name' do + expect(configuration).not_to receive(:save) + + result + + expect(configuration.reload.name).to eq(original_name) + end + end + + context 'with duplicate name' do + before do + create(:auth_app_configuration, user:, name:) + end + + it 'returns an unsuccessful result' do + expect(result.success?).to eq(false) + expect(result.to_h).to eq( + success: false, + error_details: { + name: { duplicate: true }, + }, + configuration_id:, + ) + end + + it 'does not save the new name' do + expect(configuration).not_to receive(:save) + + result + + expect(configuration.reload.name).to eq(original_name) + end + end + end + + describe '#name' do + subject(:name) { form.name } + + it 'returns configuration name' do + expect(name).to eq(configuration.name) + end + end + + describe '#configuration' do + subject(:form_configuration) { form.configuration } + + it 'returns configuration' do + expect(form_configuration).to eq(configuration) + end + end +end diff --git a/spec/forms/webauthn_setup_form_spec.rb b/spec/forms/webauthn_setup_form_spec.rb index e4680ec6795..e467c404ec0 100644 --- a/spec/forms/webauthn_setup_form_spec.rb +++ b/spec/forms/webauthn_setup_form_spec.rb @@ -171,7 +171,11 @@ expect(subject.submit(protocol, params).to_h).to eq( success: false, - errors: {}, + errors: { name: [I18n.t( + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), + )] }, + error_details: { name: { attestation_error: true } }, **extra_attributes, ) end @@ -213,8 +217,8 @@ expect(subject.submit(protocol, params).to_h).to eq( success: false, errors: { name: [I18n.t( - 'errors.webauthn_setup.attestation_error', - link: MarketingSite.contact_url, + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), )] }, error_details: { name: { attestation_error: true } }, **extra_attributes, diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 7c9e0965fbf..ca5ac68f823 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -17,6 +17,7 @@ let(:threatmetrix_session_id) { SecureRandom.uuid } let(:proofing_device_profiling) { :enabled } let(:lexisnexis_threatmetrix_mock_enabled) { false } + let(:ipp_enrollment_in_progress) { false } before do allow(IdentityConfig.store).to receive(:proofing_device_profiling). @@ -40,6 +41,7 @@ user_id: user.id, threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) end @@ -118,6 +120,7 @@ user_id: user.id, threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) end it 'stores a successful result' do @@ -378,6 +381,7 @@ context "when the user's state ID address does not match their residential address" do let(:pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } + let(:ipp_enrollment_in_progress) { true } let(:residential_address) do { @@ -411,7 +415,7 @@ user_id: user.id, threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, - ipp_enrollment_in_progress: true, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) end @@ -510,6 +514,7 @@ context 'without a threatmetrix session ID' do let(:threatmetrix_session_id) { nil } + let(:ipp_enrollment_in_progress) { false } it 'does not make a request to threatmetrix' do stub_vendor_requests diff --git a/spec/models/disposable_domain_spec.rb b/spec/models/disposable_domain_spec.rb deleted file mode 100644 index 3572d1b0859..00000000000 --- a/spec/models/disposable_domain_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'rails_helper' - -RSpec.describe DisposableDomain do - let(:domain) { 'temporary.com' } - - describe '.disposable?' do - before do - DisposableDomain.create(name: domain) - end - - context 'when the domain exists' do - it 'returns true' do - expect(DisposableDomain.disposable?(domain)).to eq true - end - end - - context 'when the domain does not exist' do - it 'returns false' do - expect(DisposableDomain.disposable?('example.com')).to eq false - end - end - - context 'with bad data' do - it 'returns false' do - expect(DisposableDomain.disposable?('')).to eq false - expect(DisposableDomain.disposable?(nil)).to eq false - expect(DisposableDomain.disposable?({})).to eq false - end - end - end -end diff --git a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb index 86da30ce531..b16b387df7d 100644 --- a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb @@ -228,7 +228,7 @@ def response_body(include_liveness) [ Group: 'PORTRAIT_MATCH_RESULT', Name: 'FaceMatchResult', - Values: [{ Value: 'Success' }], + Values: [{ Value: 'Pass' }], ] end ), @@ -262,7 +262,7 @@ def response_body_with_doc_auth_errors(include_liveness) [ Group: 'PORTRAIT_MATCH_RESULT', Name: 'FaceMatchResult', - Values: [{ Value: 'Success' }], + Values: [{ Value: 'Pass' }], ] end ), diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 838d010d97b..6e95a753411 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -2,40 +2,27 @@ RSpec.describe DocAuth::LexisNexis::Responses::TrueIdResponse do let(:success_response_body) { LexisNexisFixtures.true_id_response_success_3 } - let(:success_with_liveness_response_body) do - LexisNexisFixtures.true_id_response_success_with_liveness - end let(:success_response) do instance_double(Faraday::Response, status: 200, body: success_response_body) end + # rubocop:disable Layout/LineLength let(:success_with_liveness_response) do - instance_double(Faraday::Response, status: 200, body: success_with_liveness_response_body) - end - let(:failure_body_no_liveness) { LexisNexisFixtures.true_id_response_failure_no_liveness } - let(:failure_body_with_liveness) { LexisNexisFixtures.true_id_response_failure_with_liveness } - let(:failure_body_with_all_failures) do - LexisNexisFixtures.true_id_response_failure_with_all_failures + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_success_with_liveness) end - let(:failure_body_no_liveness_low_dpi) do - LexisNexisFixtures.true_id_response_failure_no_liveness_low_dpi + let(:failure_response_face_match_fail) do + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_with_face_match_fail) end - - let(:failure_body_tampering) do - LexisNexisFixtures.true_id_response_failure_tampering - end - - # rubocop:disable Layout/LineLength let(:failure_response_no_liveness) do - instance_double(Faraday::Response, status: 200, body: failure_body_no_liveness) + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_failure_no_liveness) end let(:failure_response_with_liveness) do - instance_double(Faraday::Response, status: 200, body: failure_body_with_liveness) + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_failure_with_liveness) end let(:failure_response_tampering) do - instance_double(Faraday::Response, status: 200, body: failure_body_tampering) + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_failure_tampering) end let(:failure_response_with_all_failures) do - instance_double(Faraday::Response, status: 200, body: failure_body_with_all_failures) + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_failure_with_all_failures) end let(:communications_error_response) do instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.communications_error) @@ -53,7 +40,7 @@ instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_barcode_read_attention) end let(:failure_response_no_liveness_low_dpi) do - instance_double(Faraday::Response, status: 200, body: failure_body_no_liveness_low_dpi) + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_failure_no_liveness_low_dpi) end # rubocop:enable Layout/LineLength @@ -692,4 +679,40 @@ def get_decision_product(resp) end end end + + describe '#successful_result?' do + context 'when all checks other than selfie pass' do + context 'and selfie check is enabled' do + liveness_checking_enabled = true + + it 'returns true with a passing selfie' do + response = described_class.new( + success_with_liveness_response, config, liveness_checking_enabled + ) + + expect(response.successful_result?).to eq(true) + end + + it 'returns false with a failing selfie' do + response = described_class.new( + failure_response_face_match_fail, config, liveness_checking_enabled + ) + + expect(response.successful_result?).to eq(false) + end + end + + context 'and selfie check is disabled' do + liveness_checking_enabled = false + + it 'returns true no matter what the value of selfie is' do + response = described_class.new( + failure_response_face_match_fail, config, liveness_checking_enabled + ) + + expect(response.successful_result?).to eq(true) + end + end + end + end end diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index 43de10130a4..86c46ab809f 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -15,6 +15,7 @@ let(:issuer) { 'fake-issuer' } let(:friendly_name) { 'fake-name' } let(:app_id) { 'fake-app-id' } + let(:ipp_enrollment_in_progress) { false } let(:agent) { Idv::Agent.new(applicant) } @@ -41,6 +42,7 @@ user_id: user.id, threatmetrix_session_id: nil, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) result = document_capture_session.load_proofing_result.result @@ -57,6 +59,7 @@ user_id: user.id, threatmetrix_session_id: nil, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) result = document_capture_session.load_proofing_result.result expect(result[:context][:stages][:state_id]).to include( @@ -82,6 +85,7 @@ user_id: user.id, threatmetrix_session_id: nil, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) result = document_capture_session.load_proofing_result.result expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] @@ -97,6 +101,7 @@ user_id: user.id, threatmetrix_session_id: nil, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) result = document_capture_session.load_proofing_result.result @@ -118,6 +123,7 @@ user_id: user.id, threatmetrix_session_id: nil, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) result = document_capture_session.load_proofing_result.result @@ -142,6 +148,7 @@ user_id: user.id, threatmetrix_session_id: nil, request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, ) result = document_capture_session.load_proofing_result.result @@ -151,6 +158,32 @@ timed_out: true, ) end + + context 'successfully proofs in IPP flow' do + let(:ipp_enrollment_in_progress) { true } + + it 'returns a successful result' do + addr = Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS + agent = Idv::Agent.new(addr.merge(uuid: user.uuid)) + agent.proof_resolution( + document_capture_session, + should_proof_state_id: true, + trace_id: trace_id, + user_id: user.id, + threatmetrix_session_id: nil, + request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, + ) + result = document_capture_session.load_proofing_result.result + expect(result[:context][:stages][:state_id]).to include( + transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID, + errors: {}, + exception: nil, + success: true, + timed_out: false, + ) + end + end end describe '#proof_address' do diff --git a/spec/services/proofing/resolution/progressive_proofer_spec.rb b/spec/services/proofing/resolution/progressive_proofer_spec.rb index 337528cfff9..8fb731b6759 100644 --- a/spec/services/proofing/resolution/progressive_proofer_spec.rb +++ b/spec/services/proofing/resolution/progressive_proofer_spec.rb @@ -1,10 +1,9 @@ require 'rails_helper' RSpec.describe Proofing::Resolution::ProgressiveProofer do - let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } + let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } let(:should_proof_state_id) { true } - let(:ipp_enrollment_in_progress) { true } - let(:double_address_verification) { true } + let(:ipp_enrollment_in_progress) { false } let(:request_ip) { Faker::Internet.ip_v4_address } let(:threatmetrix_session_id) { SecureRandom.uuid } let(:timer) { JobHelpers::Timer.new } @@ -32,6 +31,21 @@ zipcode: applicant_pii[:zipcode], } end + let(:transformed_pii) do + { + first_name: 'FAKEY', + last_name: 'MCFAKERSON', + dob: '1938-10-06', + address1: '123 Way St', + address2: '2nd Address Line', + city: 'Best City', + zipcode: '12345', + state_id_jurisdiction: 'Virginia', + address_state: 'VA', + state_id_number: '1111111111111', + same_address_as_id: 'true', + } + end describe '#proof' do before do @@ -42,7 +56,6 @@ instance.proof( applicant_pii: applicant_pii, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - double_address_verification: double_address_verification, request_ip: request_ip, should_proof_state_id: should_proof_state_id, threatmetrix_session_id: threatmetrix_session_id, @@ -51,40 +64,63 @@ ) end - it 'returns a ResultAdjudicator' do - proofing_result = proof + context 'remote proofing' do + it 'returns a ResultAdjudicator' do + proofing_result = proof - expect(proofing_result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(proofing_result.same_address_as_id).to eq(applicant_pii[:same_address_as_id]) - end + expect(proofing_result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) + expect(proofing_result.same_address_as_id).to eq(nil) + end - let(:resolution_result) do - instance_double(Proofing::Resolution::Result) - end - context 'ThreatMetrix is enabled' do - let(:threatmetrix_proofer) { instance_double(Proofing::LexisNexis::Ddp::Proofer) } - - before do - allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?). - and_return(true) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled). - and_return(false) - allow(instance).to receive(:lexisnexis_ddp_proofer).and_return(threatmetrix_proofer) - - allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). - and_return(resolution_result) - allow(resolution_result).to receive(:success?).and_return(true) - allow(instant_verify_proofer).to receive(:proof) + let(:resolution_result) do + instance_double(Proofing::Resolution::Result) end + context 'ThreatMetrix is enabled' do + let(:threatmetrix_proofer) { instance_double(Proofing::LexisNexis::Ddp::Proofer) } + + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?). + and_return(true) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(false) + allow(instance).to receive(:lexisnexis_ddp_proofer).and_return(threatmetrix_proofer) - it 'makes a request to the ThreatMetrix proofer' do - expect(threatmetrix_proofer).to receive(:proof) + allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). + and_return(resolution_result) + allow(resolution_result).to receive(:success?).and_return(true) + allow(instant_verify_proofer).to receive(:proof) + end + + it 'makes a request to the ThreatMetrix proofer' do + expect(threatmetrix_proofer).to receive(:proof) + + subject + end - subject + context 'it lacks a session id' do + let(:threatmetrix_session_id) { nil } + it 'returns a disabled result' do + result = subject + + device_profiling_result = result.device_profiling_result + + expect(device_profiling_result.success).to be(true) + expect(device_profiling_result.client).to eq('tmx_disabled') + expect(device_profiling_result.review_status).to eq('pass') + end + end end - context 'it lacks a session id' do - let(:threatmetrix_session_id) { nil } + context 'ThreatMetrix is disabled' do + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?). + and_return(false) + + allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). + and_return(resolution_result) + allow(resolution_result).to receive(:success?).and_return(true) + allow(instant_verify_proofer).to receive(:proof) + end it 'returns a disabled result' do result = subject @@ -95,363 +131,358 @@ expect(device_profiling_result.review_status).to eq('pass') end end - end - context 'ThreatMetrix is disabled' do - before do - allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?). - and_return(false) + context 'LexisNexis Instant Verify A/B test enabled' do + let(:residential_instant_verify_proof) do + instance_double(Proofing::Resolution::Result) + end + let(:instant_verify_workflow) { 'equitable_workflow' } + let(:ab_test_variables) do + { + ab_testing_enabled: true, + use_alternate_workflow: true, + instant_verify_workflow: instant_verify_workflow, + } + end + + before do + allow(instant_verify_proofer).to receive(:proof). + and_return(residential_instant_verify_proof) + allow(residential_instant_verify_proof).to receive(:success?).and_return(true) + end - allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). - and_return(resolution_result) - allow(resolution_result).to receive(:success?).and_return(true) - allow(instant_verify_proofer).to receive(:proof) + it 'uses the selected workflow' do + lniv = Idv::LexisNexisInstantVerify.new(dcs_uuid) + expect(lniv).to receive(:workflow_ab_testing_variables). + and_return(ab_test_variables) + expect(Idv::LexisNexisInstantVerify).to receive(:new). + and_return(lniv) + expect(Proofing::LexisNexis::InstantVerify::Proofer).to receive(:new). + with(hash_including(instant_verify_workflow: instant_verify_workflow)). + and_return(instant_verify_proofer) + + proof + end end - it 'returns a disabled result' do - result = subject - device_profiling_result = result.device_profiling_result + context 'remote flow does not augment pii' do + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } + let(:id_address_instant_verify_proof) do + instance_double(Proofing::Resolution::Result) + end - expect(device_profiling_result.success).to be(true) - expect(device_profiling_result.client).to eq('tmx_disabled') - expect(device_profiling_result.review_status).to eq('pass') - end - end + before do + allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) + allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) + allow(instant_verify_proofer).to receive(:proof). + and_return(id_address_instant_verify_proof) + allow(id_address_instant_verify_proof).to receive(:success?).and_return(true) + end - context 'LexisNexis Instant Verify A/B test enabled' do - let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } - let(:residential_instant_verify_proof) do - instance_double(Proofing::Resolution::Result) - end - let(:instant_verify_workflow) { 'equitable_workflow' } - let(:ab_test_variables) do - { - ab_testing_enabled: true, - use_alternate_workflow: true, - instant_verify_workflow: instant_verify_workflow, - } - end + it 'proofs with untransformed pii' do + expect(aamva_proofer).to receive(:proof).with(applicant_pii) - before do - allow(instant_verify_proofer).to receive(:proof). - and_return(residential_instant_verify_proof) - allow(residential_instant_verify_proof).to receive(:success?).and_return(true) - end + result = subject - it 'uses the selected workflow' do - lniv = Idv::LexisNexisInstantVerify.new(dcs_uuid) - expect(lniv).to receive(:workflow_ab_testing_variables). - and_return(ab_test_variables) - expect(Idv::LexisNexisInstantVerify).to receive(:new). - and_return(lniv) - expect(Proofing::LexisNexis::InstantVerify::Proofer).to receive(:new). - with(hash_including(instant_verify_workflow: instant_verify_workflow)). - and_return(instant_verify_proofer) - - proof + expect(result.same_address_as_id).to eq(nil) + expect(result.ipp_enrollment_in_progress).to eq(false) + # rubocop:disable Layout/LineLength + expect(result.residential_resolution_result.vendor_name).to eq('ResidentialAddressNotRequired') + # rubocop:enable Layout/LineLength + end end end - context 'residential address and id address are the same' do + context 'ipp flow' do + let(:ipp_enrollment_in_progress) { true } let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } - let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } - let(:residential_instant_verify_proof) do - instance_double(Proofing::Resolution::Result) - end - before do - allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) - allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) - allow(instant_verify_proofer).to receive(:proof). - and_return(residential_instant_verify_proof) - allow(residential_instant_verify_proof).to receive(:success?).and_return(true) - end - it 'only makes one request to LexisNexis InstantVerify' do - expect(instant_verify_proofer).to receive(:proof).exactly(:once) - expect(aamva_proofer).to receive(:proof) + it 'returns a ResultAdjudicator' do + proofing_result = proof - subject + expect(proofing_result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) + expect(proofing_result.same_address_as_id).to eq(applicant_pii[:same_address_as_id]) end - it 'produces a result adjudicator with correct information' do - expect(aamva_proofer).to receive(:proof) - - result = subject - - expect(result.same_address_as_id).to eq('true') - expect(result.ipp_enrollment_in_progress).to eq(true) - expect(result.double_address_verification).to eq(true) - expect(result.resolution_result).to eq(result.residential_resolution_result) - end - - context 'LexisNexis InstantVerify fails' do - let(:result_that_failed_instant_verify) do + context 'residential address and id address are the same' do + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } + let(:residential_instant_verify_proof) do instance_double(Proofing::Resolution::Result) end before do - allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). - and_return(result_that_failed_instant_verify) - allow(instant_verify_proofer).to receive(:proof).with(hash_including(state_id_address)). - and_return(result_that_failed_instant_verify) - allow(instance).to receive(:user_can_pass_after_state_id_check?). - with(result_that_failed_instant_verify). - and_return(true) - allow(result_that_failed_instant_verify).to receive(:success?). - and_return(false) + allow(instance).to receive(:with_state_id_address).and_return(transformed_pii) + allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) + allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) + allow(instant_verify_proofer).to receive(:proof). + and_return(residential_instant_verify_proof) + allow(residential_instant_verify_proof).to receive(:success?).and_return(true) end - context 'the failure can be covered by AAMVA' do - before do - allow(result_that_failed_instant_verify). - to receive(:attributes_requiring_additional_verification). - and_return([:address]) - end + it 'only makes one request to LexisNexis InstantVerify' do + expect(instant_verify_proofer).to receive(:proof).exactly(:once) + expect(aamva_proofer).to receive(:proof) - context 'it is not covered by AAMVA' do - let(:failed_aamva_proof) { instance_double(Proofing::StateIdResult) } - before do - allow(aamva_proofer).to receive(:proof).and_return(failed_aamva_proof) - allow(failed_aamva_proof).to receive(:verified_attributes).and_return([]) - allow(failed_aamva_proof).to receive(:success?).and_return(false) - end - it 'indicates the aamva check did not pass' do - result = subject + subject + end - expect(result.state_id_result.success?).to eq(false) - end - end + it 'produces a result adjudicator with correct information' do + expect(aamva_proofer).to receive(:proof) - context 'it is covered by AAMVA' do - let(:successful_aamva_proof) { instance_double(Proofing::StateIdResult) } - before do - allow(aamva_proofer).to receive(:proof).and_return(successful_aamva_proof) - allow(successful_aamva_proof).to receive(:verified_attributes). - and_return([:address]) - allow(successful_aamva_proof).to receive(:success?).and_return(true) - end - it 'indicates aamva did pass' do - result = subject + result = subject + expect(result.same_address_as_id).to eq('true') + expect(result.ipp_enrollment_in_progress).to eq(true) + expect(result.resolution_result).to eq(result.residential_resolution_result) + end - expect(result.state_id_result.success?).to eq(true) - end - end + it 'transforms PII correctly' do + expect(aamva_proofer).to receive(:proof).with(transformed_pii) + + result = subject + expect(result.same_address_as_id).to eq('true') + expect(result.ipp_enrollment_in_progress).to eq(true) + expect(result.resolution_result).to eq(result.residential_resolution_result) + expect(result.resolution_result.success?).to eq(true) end - end - context 'LexisNexis InstantVerify passes for residential address and id address' do - context 'should proof with AAMVA' do - let(:id_resolution_that_passed_instant_verify) do + context 'LexisNexis InstantVerify fails' do + let(:result_that_failed_instant_verify) do instance_double(Proofing::Resolution::Result) end - let(:residential_resolution_that_passed_instant_verify) do - instance_double(Proofing::Resolution::Result) - end - before do - allow(instance).to receive(:proof_residential_address_if_needed). - and_return(residential_resolution_that_passed_instant_verify) allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). - and_return(id_resolution_that_passed_instant_verify) - allow(instant_verify_proofer).to receive(:proof). - with(hash_including(state_id_address)). - and_return(id_resolution_that_passed_instant_verify) + and_return(result_that_failed_instant_verify) + allow(instant_verify_proofer).to receive(:proof).with(hash_including(state_id_address)). + and_return(result_that_failed_instant_verify) allow(instance).to receive(:user_can_pass_after_state_id_check?). - with(id_resolution_that_passed_instant_verify). - and_return(true) - allow(id_resolution_that_passed_instant_verify).to receive(:success?). - and_return(true) - allow(residential_resolution_that_passed_instant_verify).to receive(:success?). + with(result_that_failed_instant_verify). and_return(true) + allow(result_that_failed_instant_verify).to receive(:success?). + and_return(false) end - it 'makes a request to the AAMVA proofer' do - expect(aamva_proofer).to receive(:proof) + context 'the failure can be covered by AAMVA' do + before do + allow(result_that_failed_instant_verify). + to receive(:attributes_requiring_additional_verification). + and_return([:address]) + end - subject + context 'it is not covered by AAMVA' do + let(:failed_aamva_proof) { instance_double(Proofing::StateIdResult) } + before do + allow(aamva_proofer).to receive(:proof).and_return(failed_aamva_proof) + allow(failed_aamva_proof).to receive(:verified_attributes).and_return([]) + allow(failed_aamva_proof).to receive(:success?).and_return(false) + end + it 'indicates the aamva check did not pass' do + result = subject + + expect(result.state_id_result.success?).to eq(false) + end + end + + context 'it is covered by AAMVA' do + let(:successful_aamva_proof) { instance_double(Proofing::StateIdResult) } + before do + allow(aamva_proofer).to receive(:proof).and_return(successful_aamva_proof) + allow(successful_aamva_proof).to receive(:verified_attributes). + and_return([:address]) + allow(successful_aamva_proof).to receive(:success?).and_return(true) + end + it 'indicates aamva did pass' do + result = subject + + expect(result.state_id_result.success?).to eq(true) + end + end end + end - context 'AAMVA proofing fails' do - let(:aamva_client) { instance_double(Proofing::Aamva::VerificationClient) } - let(:failed_aamva_proof) do - instance_double(Proofing::StateIdResult) + context 'LexisNexis InstantVerify passes for residential address and id address' do + context 'should proof with AAMVA' do + let(:id_resolution_that_passed_instant_verify) do + instance_double(Proofing::Resolution::Result) + end + let(:residential_resolution_that_passed_instant_verify) do + instance_double(Proofing::Resolution::Result) end + before do - allow(Proofing::Aamva::VerificationClient).to receive(:new).and_return(aamva_client) - allow(failed_aamva_proof).to receive(:success?).and_return(false) + allow(instance).to receive(:proof_residential_address_if_needed). + and_return(residential_resolution_that_passed_instant_verify) + allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). + and_return(id_resolution_that_passed_instant_verify) + allow(instant_verify_proofer).to receive(:proof). + with(hash_including(state_id_address)). + and_return(id_resolution_that_passed_instant_verify) + allow(instance).to receive(:user_can_pass_after_state_id_check?). + with(id_resolution_that_passed_instant_verify). + and_return(true) + allow(id_resolution_that_passed_instant_verify).to receive(:success?). + and_return(true) + allow(residential_resolution_that_passed_instant_verify).to receive(:success?). + and_return(true) end - it 'returns a result adjudicator that indicates the aamva proofing failed' do - allow(aamva_proofer).to receive(:proof).and_return(failed_aamva_proof) - result = subject + it 'makes a request to the AAMVA proofer' do + expect(aamva_proofer).to receive(:proof) - expect(result.state_id_result.success?).to eq(false) + subject + end + + context 'AAMVA proofing fails' do + let(:aamva_client) { instance_double(Proofing::Aamva::VerificationClient) } + let(:failed_aamva_proof) do + instance_double(Proofing::StateIdResult) + end + before do + allow(Proofing::Aamva::VerificationClient).to receive(:new).and_return(aamva_client) + allow(failed_aamva_proof).to receive(:success?).and_return(false) + end + it 'returns a result adjudicator that indicates the aamva proofing failed' do + allow(aamva_proofer).to receive(:proof).and_return(failed_aamva_proof) + + result = subject + + expect(result.state_id_result.success?).to eq(false) + end end end end end - end - - context 'residential address and id address are different' do - let(:residential_address_proof) do - instance_double(Proofing::Resolution::Result) - end - let(:resolution_result) do - instance_double(Proofing::Resolution::Result) - end - let(:ipp_enrollment_in_progress) { true } - let(:double_address_verification) { true } - let(:applicant_pii) do - JSON.parse(<<-STR, symbolize_names: true) - { - "uuid": "3e8db152-4d35-4207-b828-3eee8c52c50f", - "middle_name": "", - "phone": "", - "state_id_expiration": "2029-01-01", - "state_id_issued": "MI", - "first_name": "Imaginary", - "last_name": "Person", - "dob": "1999-09-00", - "identity_doc_address1": "1 Seaview", - "identity_doc_address2": "", - "identity_doc_city": "Sant Cruz", - "identity_doc_zipcode": "91000", - "state_id_jurisdiction": "AZ", - "identity_doc_address_state": "CA", - "state_id_number": "AZ333222111", - "same_address_as_id": "false", - "state": "MI", - "zipcode": "48880", - "city": "Pontiac", - "address1": "1 Mobile Dr", - "address2": "", - "ssn": "900-32-1898", - "state_id_type": "drivers_license", - "uuid_prefix": null - } - STR - end - let(:residential_address) do - { - address1: applicant_pii[:address1], - address2: applicant_pii[:address2], - city: applicant_pii[:city], - state: applicant_pii[:state], - state_id_jurisdiction: applicant_pii[:state_id_jurisdiction], - zipcode: applicant_pii[:zipcode], - } - end - let(:state_id_address) do - { - address1: applicant_pii[:identity_doc_address1], - address2: applicant_pii[:identity_doc_address2], - city: applicant_pii[:identity_doc_city], - state: applicant_pii[:identity_doc_address_state], - state_id_jurisdiction: applicant_pii[:state_id_jurisdiction], - zipcode: applicant_pii[:identity_doc_zipcode], - } - end - context 'LexisNexis InstantVerify passes for residential address' do - before do - allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) - allow(instant_verify_proofer).to receive(:proof).and_return(residential_address_proof) - allow(residential_address_proof).to receive(:success?).and_return(true) + context 'residential address and id address are different' do + let(:residential_address_proof) do + instance_double(Proofing::Resolution::Result) + end + let(:resolution_result) do + instance_double(Proofing::Resolution::Result) + end + let(:ipp_enrollment_in_progress) { true } + let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } + let(:residential_address) do + { + address1: applicant_pii[:address1], + address2: applicant_pii[:address2], + city: applicant_pii[:city], + state: applicant_pii[:state], + state_id_jurisdiction: applicant_pii[:state_id_jurisdiction], + zipcode: applicant_pii[:zipcode], + } + end + let(:state_id_address) do + { + address1: applicant_pii[:identity_doc_address1], + address2: applicant_pii[:identity_doc_address2], + city: applicant_pii[:identity_doc_city], + state: applicant_pii[:identity_doc_address_state], + state_id_jurisdiction: applicant_pii[:state_id_jurisdiction], + zipcode: applicant_pii[:identity_doc_zipcode], + } end - context 'LexisNexis InstantVerify passes for id address' do - it 'makes two requests to the InstantVerify Proofer' do - expect(instant_verify_proofer).to receive(:proof). - with(hash_including(residential_address)). - ordered - expect(instant_verify_proofer).to receive(:proof). - with(hash_including(state_id_address)). - ordered - - subject + context 'LexisNexis InstantVerify passes for residential address' do + before do + allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) + allow(instant_verify_proofer).to receive(:proof).and_return(residential_address_proof) + allow(residential_address_proof).to receive(:success?).and_return(true) end - context 'AAMVA fails' do - let(:failed_aamva_proof) { instance_double(Proofing::StateIdResult) } - let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } - before do - allow(instance).to receive(:proof_id_with_aamva_if_needed). - and_return(failed_aamva_proof) - allow(aamva_proofer).to receive(:proof).and_return(failed_aamva_proof) - allow(failed_aamva_proof).to receive(:success?).and_return(false) - allow(resolution_result).to receive(:errors) - end + context 'LexisNexis InstantVerify passes for id address' do + it 'makes two requests to the InstantVerify Proofer' do + expect(instant_verify_proofer).to receive(:proof). + with(hash_including(residential_address)). + ordered + expect(instant_verify_proofer).to receive(:proof). + with(hash_including(state_id_address)). + ordered - it 'returns the correct resolution results' do - result_adjudicator = subject + subject + end - expect(result_adjudicator.residential_resolution_result.success?).to be(true) - expect(result_adjudicator.resolution_result.success?).to be(true) - expect(result_adjudicator.state_id_result.success?).to be(false) + context 'AAMVA fails' do + let(:failed_aamva_proof) { instance_double(Proofing::StateIdResult) } + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } + before do + allow(instance).to receive(:proof_id_with_aamva_if_needed). + and_return(failed_aamva_proof) + allow(aamva_proofer).to receive(:proof).and_return(failed_aamva_proof) + allow(failed_aamva_proof).to receive(:success?).and_return(false) + allow(resolution_result).to receive(:errors) + end + + it 'returns the correct resolution results' do + result_adjudicator = subject + + expect(result_adjudicator.residential_resolution_result.success?).to be(true) + expect(result_adjudicator.resolution_result.success?).to be(true) + expect(result_adjudicator.state_id_result.success?).to be(false) + end end end end - end - - context 'LexisNexis InstantVerify fails for residential address' do - let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } - - before do - allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) - allow(instance).to receive(:proof_residential_address_if_needed). - and_return(residential_address_proof) - allow(instant_verify_proofer).to receive(:proof). - with(hash_including(residential_address)). - and_return(residential_address_proof) - allow(instance).to receive(:user_can_pass_after_state_id_check?). - with(residential_address_proof). - and_return(false) - allow(residential_address_proof).to receive(:success?). - and_return(false) - end - it 'does not make unnecessary calls' do - expect(aamva_proofer).to_not receive(:proof) - expect(instant_verify_proofer).to_not receive(:proof). - with(hash_including(state_id_address)) + context 'LexisNexis InstantVerify fails for residential address' do + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } - subject - end - end + before do + allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) + allow(instance).to receive(:proof_residential_address_if_needed). + and_return(residential_address_proof) + allow(instant_verify_proofer).to receive(:proof). + with(hash_including(residential_address)). + and_return(residential_address_proof) + allow(instance).to receive(:user_can_pass_after_state_id_check?). + with(residential_address_proof). + and_return(false) + allow(residential_address_proof).to receive(:success?). + and_return(false) + end - context 'LexisNexis InstantVerify fails for id address & passes for residential address' do - let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } - let(:result_that_failed_instant_verify) do - instance_double(Proofing::Resolution::Result) - end + it 'does not make unnecessary calls' do + expect(aamva_proofer).to_not receive(:proof) + expect(instant_verify_proofer).to_not receive(:proof). + with(hash_including(state_id_address)) - before do - allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) - allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). - and_return(result_that_failed_instant_verify) - allow(instant_verify_proofer).to receive(:proof).with(hash_including(state_id_address)). - and_return(result_that_failed_instant_verify) + subject + end end - context 'the failure can be covered by AAMVA' do - let(:failed_aamva_proof) { instance_double(Proofing::StateIdResult) } + context 'LexisNexis InstantVerify fails for id address & passes for residential address' do let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } - before do - allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) - allow(instant_verify_proofer).to receive(:proof).and_return(residential_address_proof) - allow(residential_address_proof).to receive(:success?).and_return(true) + let(:result_that_failed_instant_verify) do + instance_double(Proofing::Resolution::Result) + end - allow(instance).to receive(:user_can_pass_after_state_id_check?). - with(result_that_failed_instant_verify). - and_return(true) - allow(result_that_failed_instant_verify). - to receive(:attributes_requiring_additional_verification). - and_return([:address]) + before do allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) + allow(instance).to receive(:proof_id_address_with_lexis_nexis_if_needed). + and_return(result_that_failed_instant_verify) + allow(instant_verify_proofer).to receive(:proof).with(hash_including(state_id_address)). + and_return(result_that_failed_instant_verify) end - it 'calls AAMVA' do - expect(aamva_proofer).to receive(:proof) - subject + context 'the failure can be covered by AAMVA' do + let(:failed_aamva_proof) { instance_double(Proofing::StateIdResult) } + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer) } + before do + allow(instance).to receive(:resolution_proofer).and_return(instant_verify_proofer) + allow(instant_verify_proofer).to receive(:proof).and_return(residential_address_proof) + allow(residential_address_proof).to receive(:success?).and_return(true) + + allow(instance).to receive(:user_can_pass_after_state_id_check?). + with(result_that_failed_instant_verify). + and_return(true) + allow(result_that_failed_instant_verify). + to receive(:attributes_requiring_additional_verification). + and_return([:address]) + allow(instance).to receive(:state_id_proofer).and_return(aamva_proofer) + end + it 'calls AAMVA' do + expect(aamva_proofer).to receive(:proof) + + subject + end end end end diff --git a/spec/services/proofing/resolution/result_adjudicator_spec.rb b/spec/services/proofing/resolution/result_adjudicator_spec.rb index 603226a7ca6..7da807de820 100644 --- a/spec/services/proofing/resolution/result_adjudicator_spec.rb +++ b/spec/services/proofing/resolution/result_adjudicator_spec.rb @@ -29,7 +29,6 @@ end let(:should_proof_state_id) { true } - let(:double_address_verification) { true } let(:ipp_enrollment_in_progress) { true } let(:same_address_as_id) { 'false' } @@ -52,7 +51,6 @@ state_id_result: state_id_result, should_proof_state_id: should_proof_state_id, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - double_address_verification: double_address_verification, device_profiling_result: device_profiling_result, same_address_as_id: same_address_as_id, ) @@ -93,33 +91,6 @@ expect(resolution_adjudication_reason).to eq(:fail_state_id) end end - - # rubocop:disable Layout/LineLength - context 'Confirm adjudication works for either double_address_verification or ipp_enrollment_in_progress' do - context 'Adjudication passes if double_address_verification is false and ipp_enrollment_in_progress is true' do - # rubocop:enable Layout/LineLength - let(:double_address_verification) { false } - let(:ipp_enrollment_in_progress) { true } - - it 'returns a successful response' do - result = subject.adjudicated_result - - expect(result.success?).to eq(true) - end - end - # rubocop:disable Layout/LineLength - context 'Adjudication passes if ipp_enrollment_in_progress is false and double_address_verification is true' do - # rubocop:enable Layout/LineLength - let(:double_address_verification) { true } - let(:ipp_enrollment_in_progress) { false } - - it 'returns a successful response' do - result = subject.adjudicated_result - - expect(result.success?).to eq(true) - end - end - end end end end diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 367a0ec9839..f8967ef86a5 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -147,6 +147,15 @@ def complete_verify_step(_user = nil) click_idv_submit_default end + def complete_steps_before_state_id_step + sign_in_and_2fa_user + begin_in_person_proofing + complete_prepare_step + complete_location_step + + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + end + def complete_all_in_person_proofing_steps(user = user_with_2fa, same_address_as_id: true) complete_prepare_step(user) complete_location_step(user) diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 2c43879c70e..ed1e55d8d72 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -2,6 +2,7 @@ module Features module SessionHelper + include JavascriptDriverHelper include PersonalKeyHelper VALID_PASSWORD = 'Val!d Pass w0rd'.freeze @@ -50,6 +51,7 @@ def sign_up_and_2fa_ial1_user def signin(email, password) allow(UserMailer).to receive(:new_device_sign_in).and_call_original visit new_user_session_path + set_hidden_field('platform_authenticator_available', 'true') fill_in_credentials_and_submit(email, password) continue_as(email, password) end @@ -729,5 +731,14 @@ def expect_branded_experience def acknowledge_backup_code_confirmation click_on t('two_factor_authentication.backup_codes.saved_backup_codes') end + + def set_hidden_field(id, value) + input = first("input##{id}", visible: false) + if javascript_enabled? + input.execute_script("this.value = #{value.to_json}") + else + input.set(value) + end + end end end diff --git a/spec/support/lexis_nexis_fixtures.rb b/spec/support/lexis_nexis_fixtures.rb index 750c9a51baf..d1929aed7ce 100644 --- a/spec/support/lexis_nexis_fixtures.rb +++ b/spec/support/lexis_nexis_fixtures.rb @@ -164,6 +164,10 @@ def true_id_response_success_with_liveness read_fixture_file_at_path('true_id/true_id_response_success_with_liveness.json') end + def true_id_response_with_face_match_fail + read_fixture_file_at_path('true_id/true_id_response_with_face_match_fail.json') + end + def true_id_response_failure_no_liveness read_fixture_file_at_path('true_id/true_id_response_failure_no_liveness.json') end diff --git a/spec/views/accounts/_auth_apps.html.erb_spec.rb b/spec/views/accounts/_auth_apps.html.erb_spec.rb new file mode 100644 index 00000000000..3160151842f --- /dev/null +++ b/spec/views/accounts/_auth_apps.html.erb_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe 'accounts/_auth_apps.html.erb' do + let(:user) do + create( + :user, + auth_app_configurations: create_list(:auth_app_configuration, 2), + ) + end + let(:user_session) { { auth_events: [] } } + + subject(:rendered) { render partial: 'accounts/auth_apps' } + + before do + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:user_session).and_return(user_session) + end + + it 'renders a list of auth apps' do + expect(rendered).to have_selector('[role="list"] [role="list-item"]', count: 2) + end +end diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index 23cf81fb0fb..22e8364a511 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -109,25 +109,6 @@ end end - context 'auth app listing and adding' do - context 'user has no auth app' do - let(:user) { create(:user, :fully_registered, :with_piv_or_cac) } - - it 'does not render auth app' do - expect(view).to_not render_template(partial: '_auth_apps') - end - end - - context 'user has an auth app' do - let(:user) { create(:user, :fully_registered, :with_authentication_app) } - it 'renders the auth app section' do - render - - expect(view).to render_template(partial: '_auth_apps') - end - end - end - context 'PIV/CAC listing and adding' do context 'user has no piv/cac' do let(:user) { create(:user, :fully_registered, :with_authentication_app) } diff --git a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb index 66441dfbd63..66baa50c0e0 100644 --- a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb +++ b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb @@ -37,15 +37,6 @@ ), ) end - - it 'contains link to disable TOTP' do - render - - expect(rendered).to have_link( - t('forms.buttons.disable'), - href: auth_app_delete_path(id: user.auth_app_configurations.first.id), - ) - end end context 'when the user does not have password_reset_profile' do diff --git a/spec/views/idv/welcome/show.html.erb_spec.rb b/spec/views/idv/welcome/show.html.erb_spec.rb index c974f132235..12cd6916e6c 100644 --- a/spec/views/idv/welcome/show.html.erb_spec.rb +++ b/spec/views/idv/welcome/show.html.erb_spec.rb @@ -3,11 +3,15 @@ RSpec.describe 'idv/welcome/show.html.erb' do let(:user_fully_authenticated) { true } let(:sp_name) { nil } + let(:selfie_required) { false } let(:user) { create(:user) } before do @decorated_sp_session = instance_double(ServiceProviderSession) allow(@decorated_sp_session).to receive(:sp_name).and_return(sp_name) + allow(@decorated_sp_session).to receive(:selfie_required?).and_return(selfie_required) + @sp_name = @decorated_sp_session.sp_name || APP_NAME + @title = t('doc_auth.headings.welcome', sp_name: @sp_name) allow(view).to receive(:decorated_sp_session).and_return(@decorated_sp_session) allow(view).to receive(:user_fully_authenticated?).and_return(user_fully_authenticated) allow(view).to receive(:user_signing_up?).and_return(false) @@ -27,45 +31,29 @@ it 'renders a link to return to the SP' do expect(rendered).to have_link(t('links.cancel')) end - end - - context 'without service provider' do - it 'renders troubleshooting options' do - render - expect(rendered).to have_link(t('idv.troubleshooting.options.supported_documents')) + it 'renders the welcome template' do + expect(rendered).to have_content(@title) + expect(rendered).to have_content(t('doc_auth.instructions.getting_started')) + expect(rendered).to have_content(t('doc_auth.instructions.bullet1')) expect(rendered).to have_link( - t('idv.troubleshooting.options.learn_more_address_verification_options'), - ) - expect(rendered).not_to have_link( - nil, - href: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), + t('doc_auth.info.getting_started_learn_more'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'how-to-verify-your-identity', + flow: :idv, + step: :welcome, + location: 'intro_paragraph', + ), ) end - end - - context 'with service provider' do - let(:sp_name) { 'Example App' } - it 'renders troubleshooting options' do - render + context 'when the SP requests IAL2 verification' do + let(:selfie_required) { true } - expect(rendered).to have_link(t('idv.troubleshooting.options.supported_documents')) - expect(rendered).to have_link( - t('idv.troubleshooting.options.learn_more_address_verification_options'), - ) - expect(rendered).to have_link( - t('idv.troubleshooting.options.get_help_at_sp', sp_name: sp_name), - href: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), - ) + it 'renders a modified welcome template' do + expect(rendered).to have_content(t('doc_auth.instructions.bullet1_with_selfie')) + end end end - - it 'renders a link to the privacy & security page' do - render - expect(rendered).to have_link( - t('doc_auth.instructions.learn_more'), - href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer), - ) - end end diff --git a/spec/views/users/auth_app/edit.html.erb_spec.rb b/spec/views/users/auth_app/edit.html.erb_spec.rb new file mode 100644 index 00000000000..55e90c0393f --- /dev/null +++ b/spec/views/users/auth_app/edit.html.erb_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +RSpec.describe 'users/auth_app/edit.html.erb' do + include Devise::Test::ControllerHelpers + + let(:nickname) { 'Example' } + let(:configuration) { create(:auth_app_configuration, name: nickname) } + let(:user) { create(:user, auth_app_configurations: [configuration]) } + let(:form) do + TwoFactorAuthentication::AuthAppUpdateForm.new( + user:, + configuration_id: configuration.id, + ) + end + + subject(:rendered) { render } + + before do + @form = form + end + + it 'renders form to update configuration' do + expect(rendered).to have_selector( + "form[action='#{auth_app_path(id: configuration.id)}'] input[name='_method'][value='put']", + visible: false, + ) + end + + it 'initializes form with configuration values' do + expect(rendered).to have_field( + t('two_factor_authentication.auth_app.nickname'), + with: nickname, + ) + end + + it 'has labelled form with button to delete configuration' do + expect(rendered).to have_button_to_with_accessibility( + t('two_factor_authentication.auth_app.delete'), + auth_app_path(id: configuration.id), + ) + end +end