diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 82ea5f102c3..2083b53f37d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -469,7 +469,7 @@ trigger_devops: - >- helm upgrade --install --namespace review-apps --debug - --set env="reviewapps" + --set env="reviewapps-$CI_ENVIRONMENT_SLUG" --set idp.image.repository="${ECR_REGISTRY}/identity-idp/review" --set idp.image.tag="${CI_COMMIT_SHA}" --set worker.image.repository="${ECR_REGISTRY}/identity-idp/review" @@ -490,7 +490,7 @@ trigger_devops: --set-json dashboard.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG-review-app-dashboard.review-app.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" $CI_ENVIRONMENT_SLUG ./identity-idp-helm-chart - echo "DNS may take a while to propagate, so be patient if it doesn't show up right away" - - echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name review_app'" + - echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name reviewapps'" - echo "Then run aws-vault exec sandbox-power -- kubectl exec -it service/$CI_ENVIRONMENT_SLUG-login-chart-idp -n review-apps -- /app/bin/rails console" - echo "Address of IDP review app:" - echo https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov diff --git a/Gemfile b/Gemfile index 4bfc01fa764..0a851079286 100644 --- a/Gemfile +++ b/Gemfile @@ -68,7 +68,7 @@ gem 'rqrcode' gem 'ruby-progressbar' gem 'ruby-saml' gem 'safe_target_blank', '>= 1.0.2' -gem 'saml_idp', github: '18F/saml_idp', tag: '0.20.2-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.21.0-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'stringex', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 2e22b77122b..7f219b5e301 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,10 +35,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: dd8643b16c8214f7b791763538180d043af7ef65 - tag: 0.20.2-18f + revision: 33275d69f7609e448942d6e3ce5c27779920995f + tag: 0.21.0-18f specs: - saml_idp (0.20.2.pre.18f) + saml_idp (0.21.0.pre.18f) activesupport builder faraday diff --git a/Makefile b/Makefile index 5fa605ec554..6137cb4b5d5 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ ARTIFACT_DESTINATION_FILE ?= ./tmp/idp.tar.gz lint_analytics_events_sorted \ lint_country_dialing_codes \ lint_erb \ + lint_font_glyphs \ lint_lockfiles \ lint_new_typescript_files \ lint_optimized_assets \ @@ -87,6 +88,8 @@ endif # Other @echo "--- lint yaml ---" make lint_yaml + @echo "--- lint font glyphs ---" + make lint_font_glyphs @echo "--- lint Yarn workspaces ---" make lint_yarn_workspaces @echo "--- lint new TypeScript files ---" @@ -110,6 +113,15 @@ lint_erb: ## Lints ERB files lint_yaml: normalize_yaml ## Lints YAML files (! git diff --name-only | grep "^config/.*\.yml$$") || (echo "Error: Run 'make normalize_yaml' to normalize YAML"; exit 1) +lint_font_glyphs: ## Lints to validate content glyphs match expectations from fonts + scripts/yaml_characters \ + --exclude-locale=zh \ + --exclude-gem-path=faker \ + --exclude-gem-path=good_job \ + --exclude-gem-path=i18n-tasks \ + > app/assets/fonts/glyphs.txt + (! git diff --name-only | grep "glyphs\.txt$$") || (echo "Error: New character data found. Follow 'Fonts' instructions in 'docs/frontend.md' to regenerate fonts."; exit 1) + lint_yarn_workspaces: ## Lints Yarn workspace packages scripts/validate-workspaces.mjs @@ -121,7 +133,7 @@ lint_asset_bundle_size: ## Lints JavaScript and CSS compiled bundle size @# budget and accept the fact that this will force end-users to endure longer load times, you @# should set the new budget to within a few thousand bytes of the production-compiled size. find app/assets/builds/application.css -size -185000c | grep . - find public/packs/js/application-*.digested.js -size -5000c | grep . + find public/packs/application-*.digested.js -size -5000c | grep . lint_migrations: scripts/migration_check diff --git a/app/assets/fonts/glyphs.txt b/app/assets/fonts/glyphs.txt new file mode 100644 index 00000000000..4c552993112 --- /dev/null +++ b/app/assets/fonts/glyphs.txt @@ -0,0 +1 @@ + !"#$%&'(),-./0123456789:;>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~ «»¿ÀÁÈÉÊÎÓÚàáâãçèéêëíîïñóôùúû ‑—‘’“”…‹中体文简 diff --git a/app/assets/fonts/public-sans/PublicSans-Black.woff b/app/assets/fonts/public-sans/PublicSans-Black.woff deleted file mode 100644 index 8174446d892..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Black.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Black.woff2 b/app/assets/fonts/public-sans/PublicSans-Black.woff2 index 656549ff192..28de4cbb742 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Black.woff2 and b/app/assets/fonts/public-sans/PublicSans-Black.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff b/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff deleted file mode 100644 index 4b3f2495856..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff2 index a77821ca359..891b65c5319 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-BlackItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-Bold.woff b/app/assets/fonts/public-sans/PublicSans-Bold.woff deleted file mode 100644 index a330a13dcf0..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Bold.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Bold.woff2 b/app/assets/fonts/public-sans/PublicSans-Bold.woff2 index eaffd02816e..3fad30fba01 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Bold.woff2 and b/app/assets/fonts/public-sans/PublicSans-Bold.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff b/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff deleted file mode 100644 index d03f7ca2295..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff2 index bdd712d81eb..172dd572175 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-BoldItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff b/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff deleted file mode 100644 index 33ceeb2fa59..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff2 b/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff2 index 0caaa499cc2..91d6ecbee2b 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff2 and b/app/assets/fonts/public-sans/PublicSans-ExtraBold.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff b/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff deleted file mode 100644 index a89787fd384..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff2 index 774fe32b0a3..4bcd956a1b6 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-ExtraBoldItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff b/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff deleted file mode 100644 index 7ae600def7e..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff2 b/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff2 index 219329aba5b..42af48d4b86 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff2 and b/app/assets/fonts/public-sans/PublicSans-ExtraLight.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff b/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff deleted file mode 100644 index 30b9103786e..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff2 index f0e21c92195..9321da5e3f8 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-ExtraLightItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-Italic.woff b/app/assets/fonts/public-sans/PublicSans-Italic.woff deleted file mode 100644 index 319cb1e8939..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Italic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Italic.woff2 b/app/assets/fonts/public-sans/PublicSans-Italic.woff2 index a96fa977705..d1bb5d63b8e 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Italic.woff2 and b/app/assets/fonts/public-sans/PublicSans-Italic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-Light.woff b/app/assets/fonts/public-sans/PublicSans-Light.woff deleted file mode 100644 index 740c94d3387..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Light.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Light.woff2 b/app/assets/fonts/public-sans/PublicSans-Light.woff2 index 87fc6df1e36..5ded7cd94f4 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Light.woff2 and b/app/assets/fonts/public-sans/PublicSans-Light.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-LightItalic.woff b/app/assets/fonts/public-sans/PublicSans-LightItalic.woff deleted file mode 100644 index 9172ccc83a8..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-LightItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-LightItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-LightItalic.woff2 index bc9b5f7edd7..263a8a0e884 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-LightItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-LightItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-Medium.woff b/app/assets/fonts/public-sans/PublicSans-Medium.woff deleted file mode 100644 index 47a26564c40..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Medium.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Medium.woff2 b/app/assets/fonts/public-sans/PublicSans-Medium.woff2 index 5896b823bdf..907a110e323 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Medium.woff2 and b/app/assets/fonts/public-sans/PublicSans-Medium.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff b/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff deleted file mode 100644 index d0eaa44d398..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff2 index 0314b97c16e..ef248d4ea6e 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-MediumItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-Regular.woff b/app/assets/fonts/public-sans/PublicSans-Regular.woff deleted file mode 100644 index 8c1da26dbe4..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Regular.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Regular.woff2 b/app/assets/fonts/public-sans/PublicSans-Regular.woff2 index 5f5fcd86111..f787a12e540 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Regular.woff2 and b/app/assets/fonts/public-sans/PublicSans-Regular.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-SemiBold.woff b/app/assets/fonts/public-sans/PublicSans-SemiBold.woff deleted file mode 100644 index 7c530b68836..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-SemiBold.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-SemiBold.woff2 b/app/assets/fonts/public-sans/PublicSans-SemiBold.woff2 index 14e63ab7528..260aeac6556 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-SemiBold.woff2 and b/app/assets/fonts/public-sans/PublicSans-SemiBold.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff b/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff deleted file mode 100644 index ea23cad8b31..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff2 index 77447b4feb7..b14cc0eaa03 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-SemiBoldItalic.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-Thin.woff b/app/assets/fonts/public-sans/PublicSans-Thin.woff deleted file mode 100644 index 74d7727bd67..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-Thin.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-Thin.woff2 b/app/assets/fonts/public-sans/PublicSans-Thin.woff2 index bdc7449f203..9587c72b5d7 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-Thin.woff2 and b/app/assets/fonts/public-sans/PublicSans-Thin.woff2 differ diff --git a/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff b/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff deleted file mode 100644 index 04dbf4147a6..00000000000 Binary files a/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff and /dev/null differ diff --git a/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff2 b/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff2 index b8b1fbb51a9..cad2b41d0dc 100644 Binary files a/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff2 and b/app/assets/fonts/public-sans/PublicSans-ThinItalic.woff2 differ diff --git a/app/assets/stylesheets/_uswds-core.scss b/app/assets/stylesheets/_uswds-core.scss index 3f9d36d90d1..726afafc28a 100644 --- a/app/assets/stylesheets/_uswds-core.scss +++ b/app/assets/stylesheets/_uswds-core.scss @@ -2,6 +2,7 @@ $theme-body-font-size: 'sm', $theme-button-icon-gap: 0.5, $theme-font-path: '', + $theme-font-weight-light: false, $theme-image-path: '', $theme-global-border-box-sizing: true, $theme-global-link-styles: true, diff --git a/app/assets/stylesheets/components/_banner.scss b/app/assets/stylesheets/components/_banner.scss deleted file mode 100644 index 5fdca668e76..00000000000 --- a/app/assets/stylesheets/components/_banner.scss +++ /dev/null @@ -1,7 +0,0 @@ -@use 'uswds-core' as *; - -.usa-banner__inner { - @include at-media('tablet') { - justify-content: center; - } -} diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index 0647f995f31..4e1b4f67ab6 100644 --- a/app/assets/stylesheets/components/_index.scss +++ b/app/assets/stylesheets/components/_index.scss @@ -1,7 +1,6 @@ @forward 'account-header'; @forward 'alert-icon'; @forward 'alert'; -@forward 'banner'; @forward 'block-link'; @forward 'btn'; @forward 'card'; diff --git a/app/assets/stylesheets/components/_step-indicator.scss b/app/assets/stylesheets/components/_step-indicator.scss index c9a4384a917..a9f4dc6c083 100644 --- a/app/assets/stylesheets/components/_step-indicator.scss +++ b/app/assets/stylesheets/components/_step-indicator.scss @@ -130,14 +130,3 @@ lg-step-indicator { .step-indicator__step--current .step-indicator__step-title { font-weight: bold; } - -.step-indicator__step-subtitle { - @include at-media-max('tablet') { - @include sr-only; - } - - @include at-media('tablet') { - display: block; - font-style: italic; - } -} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 508f3c8f0f2..b83acd71750 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -111,6 +111,7 @@ def resolved_authn_context_result @resolved_authn_context_result = Vot::Parser::Result.no_sp_result else @resolved_authn_context_result = AuthnContextResolver.new( + user: current_user, service_provider: service_provider, vtr: sp_session[:vtr], acr_values: sp_session[:acr_values], diff --git a/app/controllers/concerns/new_device_concern.rb b/app/controllers/concerns/new_device_concern.rb new file mode 100644 index 00000000000..b96b1f64d05 --- /dev/null +++ b/app/controllers/concerns/new_device_concern.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module NewDeviceConcern + def set_new_device_session + user_session[:new_device] = !current_user.authenticated_device?(cookie_uuid: cookies[:device]) + end + + def new_device? + user_session[:new_device] != false + end +end diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index a40172b71ce..ed7b993576a 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -23,7 +23,7 @@ module SamlIdpAuthConcern private def block_biometric_requests_in_production - if @saml_request_validator.parsed_vector_of_trust&.biometric_comparison? && + if @saml_request_validator.biometric_comparison_requested? && !FeatureManagement.idv_allow_selfie_check? render_not_acceptable end @@ -130,9 +130,12 @@ def default_ial_context end def response_authn_context - saml_request.requested_vtr_authn_context || + if saml_request.requested_vtr_authn_contexts.present? + resolved_authn_context_result.expanded_component_values + else saml_request.requested_aal_authn_context || - default_aal_context + default_aal_context + end end def requested_ial_authn_context diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index e717304c48e..c4eea88da74 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -5,6 +5,7 @@ module TwoFactorAuthenticatableMethods include RememberDeviceConcern include SecureHeadersConcern include MfaSetupConcern + include NewDeviceConcern def auth_methods_session @auth_methods_session ||= AuthMethodsSession.new(user_session:) @@ -14,8 +15,7 @@ def handle_valid_verification_for_authentication_context(auth_method:) mark_user_session_authenticated(auth_method:, authentication_type: :valid_2fa) disavowal_event, disavowal_token = create_user_event_with_disavowal(:sign_in_after_2fa) - if IdentityConfig.store.feature_new_device_alert_aggregation_enabled && - user_session[:new_device] != false + if IdentityConfig.store.feature_new_device_alert_aggregation_enabled && new_device? if current_user.sign_in_new_device_at.blank? current_user.update(sign_in_new_device_at: disavowal_event.created_at) end diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index 7eb7df6926d..f18bd4492b9 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -26,7 +26,10 @@ def update clear_future_steps! skip_to_capture if params[:skip_hybrid_handoff] - result = Idv::ConsentForm.new.submit(consent_form_params) + @consent_form = Idv::ConsentForm.new( + idv_consent_given: idv_session.idv_consent_given, + ) + result = @consent_form.submit(consent_form_params) analytics.idv_doc_auth_agreement_submitted( **analytics_arguments.merge(result.to_h), @@ -42,7 +45,7 @@ def update redirect_to idv_hybrid_handoff_url end else - redirect_to idv_agreement_url + render :show end end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 5325cb30c62..d7a98770153 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -53,7 +53,7 @@ def extra_view_variables sp_name: decorated_sp_session.sp_name, failure_to_proof_url: return_to_sp_failure_to_proof_url(step: 'document_capture'), skip_doc_auth: idv_session.skip_doc_auth, - skip_doc_auth_from_how_to_verify: false, + skip_doc_auth_from_how_to_verify: idv_session.skip_doc_auth_from_how_to_verify, skip_doc_auth_from_handoff: idv_session.skip_doc_auth_from_handoff, opted_in_to_in_person_proofing: idv_session.opted_in_to_in_person_proofing, doc_auth_selfie_capture:, @@ -73,6 +73,7 @@ def self.step_info idv_session.skip_doc_auth_from_handoff || idv_session.skip_hybrid_handoff || idv_session.skip_doc_auth || + idv_session.skip_doc_auth_from_how_to_verify || !idv_session.selfie_check_required || # desktop but selfie not required idv_session.desktop_selfie_test_mode_enabled? ) diff --git a/app/controllers/idv/how_to_verify_controller.rb b/app/controllers/idv/how_to_verify_controller.rb index e9abd7d9f92..a41f98da563 100644 --- a/app/controllers/idv/how_to_verify_controller.rb +++ b/app/controllers/idv/how_to_verify_controller.rb @@ -39,11 +39,13 @@ def update if how_to_verify_form_params['selection'] == Idv::HowToVerifyForm::REMOTE idv_session.opted_in_to_in_person_proofing = false idv_session.skip_doc_auth = false + idv_session.skip_doc_auth_from_how_to_verify = false redirect_to idv_hybrid_handoff_url else idv_session.opted_in_to_in_person_proofing = true idv_session.flow_path = 'standard' idv_session.skip_doc_auth = true + idv_session.skip_doc_auth_from_how_to_verify = true redirect_to idv_document_capture_url end @@ -70,6 +72,7 @@ def self.step_info end, undo_step: ->(idv_session:, user:) { idv_session.skip_doc_auth = nil + idv_session.skip_doc_auth_from_how_to_verify = nil idv_session.opted_in_to_in_person_proofing = nil }, ) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index ba99e2d240c..3ccf028c36e 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -53,7 +53,8 @@ def self.selected_remote(idv_session:) idv_session.service_provider&.in_person_proofing_enabled idv_session.skip_doc_auth == false else - idv_session.skip_doc_auth.nil? || idv_session.skip_doc_auth == false + idv_session.skip_doc_auth.nil? || + idv_session.skip_doc_auth == false end end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index fa34a72a59d..85e80de2676 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -65,7 +65,7 @@ def block_biometric_requests_in_production end def biometric_comparison_requested? - @authorize_form.parsed_vector_of_trust&.biometric_comparison? + @authorize_form.biometric_comparison_requested? end def check_sp_active @@ -93,17 +93,31 @@ def set_devise_failure_redirect_for_concurrent_session_logout end def link_identity_to_service_provider - @authorize_form.link_identity_to_service_provider(current_user, session.id) + @authorize_form.link_identity_to_service_provider( + current_user: current_user, + ial: resolved_authn_context_int_ial, + rails_session_id: session.id, + ) end def ial_context IalContext.new( - ial: @authorize_form.ial, + ial: resolved_authn_context_int_ial, service_provider: @authorize_form.service_provider, user: current_user, ) end + def resolved_authn_context_int_ial + if resolved_authn_context_result.ialmax? + 0 + elsif resolved_authn_context_result.identity_proofing? + 2 + else + 1 + end + end + def handle_successful_handoff track_events SpHandoffBounce::AddHandoffTimeToSession.call(sp_session) diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index db8368526eb..9a9de7d9068 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -142,7 +142,7 @@ def log_external_saml_auth_request requested_ial: requested_ial, authn_context: saml_request&.requested_authn_contexts, requested_aal_authn_context: saml_request&.requested_aal_authn_context, - requested_vtr_authn_context: saml_request&.requested_vtr_authn_context, + requested_vtr_authn_contexts: saml_request&.requested_vtr_authn_contexts.presence, force_authn: saml_request&.force_authn?, final_auth_request: sp_session[:final_auth_request], service_provider: saml_request&.issuer, diff --git a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb index 4e8c05c55e3..10bcc18e140 100644 --- a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb +++ b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb @@ -3,6 +3,7 @@ module TwoFactorAuthentication class BackupCodeVerificationController < ApplicationController include TwoFactorAuthenticatable + include NewDeviceConcern prepend_before_action :authenticate_user before_action :check_sp_required_mfa @@ -22,7 +23,7 @@ def create @backup_code_form = BackupCodeVerificationForm.new(current_user) result = @backup_code_form.submit(backup_code_params) analytics.track_mfa_submit_event( - result.to_h.merge(new_device: user_session[:new_device]), + result.to_h.merge(new_device: new_device?), ) irs_attempts_api_tracker.mfa_login_backup_code(success: result.success?) handle_result(result) @@ -36,9 +37,7 @@ def all_codes_used? def handle_last_code generator = BackupCodeGenerator.new(current_user) - generator.delete_existing_codes - user_session[:backup_codes] = generator.generate - generator.save(user_session[:backup_codes]) + user_session[:backup_codes] = generator.delete_and_regenerate flash[:info] = t('forms.backup_code.last_code') redirect_to backup_code_refreshed_url end diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index 31d836e8c88..3bf40093f41 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -4,6 +4,7 @@ module TwoFactorAuthentication class OtpVerificationController < ApplicationController include TwoFactorAuthenticatable include MfaSetupConcern + include NewDeviceConcern before_action :check_sp_required_mfa before_action :confirm_multiple_factors_enabled @@ -132,7 +133,7 @@ def form_params end def post_analytics(result) - properties = result.to_h.merge(analytics_properties, new_device: user_session[:new_device]) + properties = result.to_h.merge(analytics_properties, new_device: new_device?) analytics.multi_factor_auth_setup(**properties) if context == 'confirmation' analytics.track_mfa_submit_event(properties) diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb index 64b0a8c85c8..85302e11991 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -3,6 +3,7 @@ module TwoFactorAuthentication class PersonalKeyVerificationController < ApplicationController include TwoFactorAuthenticatable + include NewDeviceConcern prepend_before_action :authenticate_user before_action :check_personal_key_enabled @@ -28,7 +29,7 @@ def track_analytics(result) analytics_hash = result.to_h.merge( multi_factor_auth_method: 'personal-key', multi_factor_auth_method_created_at: mfa_created_at&.strftime('%s%L'), - new_device: user_session[:new_device], + new_device: new_device?, ) analytics.track_mfa_submit_event(analytics_hash) diff --git a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb index 39b89f881fa..56151472bfa 100644 --- a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb +++ b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb @@ -4,6 +4,7 @@ module TwoFactorAuthentication class PivCacVerificationController < ApplicationController include TwoFactorAuthenticatable include PivCacConcern + include NewDeviceConcern before_action :confirm_piv_cac_enabled, only: :show before_action :reset_attempt_count_if_user_no_longer_locked_out, only: :show @@ -105,7 +106,7 @@ def analytics_properties context: context, multi_factor_auth_method: 'piv_cac', piv_cac_configuration_id: piv_cac_verification_form&.piv_cac_configuration&.id, - new_device: user_session[:new_device], + new_device: new_device?, } end end diff --git a/app/controllers/two_factor_authentication/totp_verification_controller.rb b/app/controllers/two_factor_authentication/totp_verification_controller.rb index eaed664380a..111c8a52c1b 100644 --- a/app/controllers/two_factor_authentication/totp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/totp_verification_controller.rb @@ -3,6 +3,7 @@ module TwoFactorAuthentication class TotpVerificationController < ApplicationController include TwoFactorAuthenticatable + include NewDeviceConcern before_action :check_sp_required_mfa before_action :confirm_totp_enabled @@ -20,7 +21,7 @@ def show def create result = TotpVerificationForm.new(current_user, params.require(:code).strip).submit - analytics.track_mfa_submit_event(result.to_h.merge(new_device: user_session[:new_device])) + analytics.track_mfa_submit_event(result.to_h.merge(new_device: new_device?)) irs_attempts_api_tracker.mfa_login_totp(success: result.success?) if result.success? diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index 57072bccfc6..458ebf650a8 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 @@ module TwoFactorAuthentication # The WebauthnVerificationController class is responsible webauthn verification at sign in class WebauthnVerificationController < ApplicationController include TwoFactorAuthenticatable + include NewDeviceConcern before_action :check_sp_required_mfa before_action :check_if_device_supports_platform_auth, only: :show @@ -22,7 +23,7 @@ def confirm **analytics_properties, multi_factor_auth_method_created_at: webauthn_configuration_or_latest.created_at.strftime('%s%L'), - new_device: user_session[:new_device], + new_device: new_device?, ) if analytics_properties[:multi_factor_auth_method] == 'webauthn_platform' diff --git a/app/controllers/users/backup_code_setup_controller.rb b/app/controllers/users/backup_code_setup_controller.rb index b01007d7f99..03580ac54cc 100644 --- a/app/controllers/users/backup_code_setup_controller.rb +++ b/app/controllers/users/backup_code_setup_controller.rb @@ -14,18 +14,30 @@ class BackupCodeSetupController < ApplicationController before_action :apply_secure_headers_override before_action :authorize_backup_code_disable, only: [:delete] before_action :confirm_recently_authenticated_2fa, except: [:reminder, :continue] - before_action :validate_internal_referrer?, only: [:index] + before_action :validate_multi_mfa_selection, only: [:index] helper_method :in_multi_mfa_selection_flow? def index + result = BackupCodeSetupForm.new(current_user).submit + visit_result = result.to_h.merge(analytics_properties_for_visit) + analytics.backup_code_setup_visit(**visit_result) + irs_attempts_api_tracker.mfa_enroll_backup_code(success: result.success?) + generate_codes + track_backup_codes_created + render :create + end + + def new; end + + def create result = BackupCodeSetupForm.new(current_user).submit visit_result = result.to_h.merge(analytics_properties_for_visit) analytics.backup_code_setup_visit(**visit_result) irs_attempts_api_tracker.mfa_enroll_backup_code(success: result.success?) - save_backup_codes + generate_codes track_backup_codes_created end @@ -43,7 +55,7 @@ def confirm_delete; end def refreshed @codes = user_session[:backup_codes] - render 'index' + render :create end def delete @@ -67,8 +79,12 @@ def confirm_backup_codes; end private - def validate_internal_referrer? - redirect_to root_url unless internal_referrer? + def validate_multi_mfa_selection + if IdentityConfig.store.backup_code_confirm_setup_screen_enabled + redirect_to backup_code_confirm_setup_url unless in_multi_mfa_selection_flow? + else + redirect_to root_url unless internal_referrer? + end end def internal_referrer? @@ -81,6 +97,13 @@ def analytics_properties_for_visit end def track_backup_codes_created + handle_valid_verification_for_confirmation_context( + auth_method: TwoFactorAuthenticatable::AuthMethod::BACKUP_CODE, + ) + event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user) + PushNotification::HttpPush.deliver(event) + create_user_event(:backup_codes_added) + analytics.backup_code_created( enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count, in_account_creation_flow: in_account_creation_flow?, @@ -98,7 +121,7 @@ def ensure_backup_codes_in_session def generate_codes revoke_remember_device(current_user) if current_user.backup_code_configurations.any? - @codes = generator.generate + @codes = generator.delete_and_regenerate user_session[:backup_codes] = @codes end @@ -111,16 +134,6 @@ def set_backup_code_setup_presenter ) end - def save_backup_codes - handle_valid_verification_for_confirmation_context( - auth_method: TwoFactorAuthenticatable::AuthMethod::BACKUP_CODE, - ) - generator.save(user_session[:backup_codes]) - event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user) - PushNotification::HttpPush.deliver(event) - create_user_event(:backup_codes_added) - end - def generator @generator ||= BackupCodeGenerator.new(current_user) end diff --git a/app/controllers/users/piv_cac_login_controller.rb b/app/controllers/users/piv_cac_login_controller.rb index 1bcb2b34b47..19a2181ef2a 100644 --- a/app/controllers/users/piv_cac_login_controller.rb +++ b/app/controllers/users/piv_cac_login_controller.rb @@ -5,6 +5,7 @@ class PivCacLoginController < ApplicationController include PivCacConcern include VerifySpAttributesConcern include TwoFactorAuthenticatableMethods + include NewDeviceConcern def new if params.key?(:token) @@ -74,7 +75,7 @@ def process_valid_submission presented: true, ) - user_session[:new_device] = current_user.new_device?(cookie_uuid: cookies[:device]) + set_new_device_session handle_valid_verification_for_authentication_context( auth_method: TwoFactorAuthenticatable::AuthMethod::PIV_CAC, ) diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 88dde2be88e..b6015b46228 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -8,6 +8,7 @@ class SessionsController < Devise::SessionsController include Ial2ProfileConcern include Api::CsrfTokenConcern include ForcedReauthenticationConcern + include NewDeviceConcern rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_signin @@ -112,8 +113,9 @@ def process_locked_out_user def handle_valid_authentication sign_in(resource_name, resource) cache_profiles(auth_params[:password]) - user_session[:new_device] = current_user.new_device?(cookie_uuid: cookies[:device]) - create_user_event(:sign_in_before_2fa) + set_new_device_session + event, = create_user_event(:sign_in_before_2fa) + UserAlerts::AlertUserAboutNewDevice.schedule_alert(event:) if new_device? EmailAddress.update_last_sign_in_at_on_user_id_and_email( user_id: current_user.id, email: auth_params[:email], diff --git a/app/forms/backup_code_verification_form.rb b/app/forms/backup_code_verification_form.rb index 3ca8e9b6cc2..2254e3db070 100644 --- a/app/forms/backup_code_verification_form.rb +++ b/app/forms/backup_code_verification_form.rb @@ -19,18 +19,19 @@ def submit(params) attr_reader :user, :backup_code def valid_backup_code? - backup_code_config.present? + valid_backup_code_config_created_at.present? end - def backup_code_config - @backup_code_config ||= BackupCodeGenerator.new(@user). - if_valid_consume_code_return_config(backup_code) + def valid_backup_code_config_created_at + return @valid_backup_code_config_created_at if defined?(@valid_backup_code_config_created_at) + @valid_backup_code_config_created_at = BackupCodeGenerator.new(@user). + if_valid_consume_code_return_config_created_at(backup_code) end def extra_analytics_attributes { multi_factor_auth_method: 'backup_code', - multi_factor_auth_method_created_at: backup_code_config&.created_at&.strftime('%s%L'), + multi_factor_auth_method_created_at: valid_backup_code_config_created_at&.strftime('%s%L'), } end end diff --git a/app/forms/idv/doc_pii_form.rb b/app/forms/idv/doc_pii_form.rb index 6b62481c092..f29c13094c6 100644 --- a/app/forms/idv/doc_pii_form.rb +++ b/app/forms/idv/doc_pii_form.rb @@ -43,6 +43,8 @@ def submit extra: { pii_like_keypaths: self.class.pii_like_keypaths, attention_with_barcode: attention_with_barcode?, + id_issued_status: pii_from_doc[:state_id_issued].present? ? 'present' : 'missing', + id_expiration_status: pii_from_doc[:state_id_expiration].present? ? 'present' : 'missing', }, ) response.pii_from_doc = pii_from_doc diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index bd3970803cd..23c4f404af0 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -91,13 +91,16 @@ def service_provider @service_provider = ServiceProvider.find_by(issuer: client_id) end - def link_identity_to_service_provider(current_user, rails_session_id) + def link_identity_to_service_provider( + current_user:, + ial:, + rails_session_id: + ) identity_linker = IdentityLinker.new(current_user, service_provider) @identity = identity_linker.link_identity( nonce: nonce, rails_session_id: rails_session_id, ial: ial, - aal: aal, acr_values: acr_values&.join(' '), vtr: vtr, requested_aal_value: requested_aal_value, @@ -117,45 +120,25 @@ def ial_values acr_values.filter { |acr| acr.include?('ial') || acr.include?('loa') } end - def ial - if parsed_vector_of_trust&.identity_proofing? - 2 - elsif parsed_vector_of_trust.present? - 1 - else - Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL[ial_values.sort.max] - end - end - def aal_values acr_values.filter { |acr| acr.include?('aal') } end - def aal - if parsed_vector_of_trust&.aal2? - 2 - elsif parsed_vector_of_trust.present? - 1 - else - Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_AAL[requested_aal_value] - end - end - def requested_aal_value highest_level_aal(aal_values) || Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF end - def biometric_comparison_required? - parsed_vector_of_trust&.biometric_comparison? + def biometric_comparison_requested? + !!parsed_vectors_of_trust&.any?(&:biometric_comparison?) end - def parsed_vector_of_trust - return @parsed_vector_of_trust if defined?(@parsed_vector_of_trust) + def parsed_vectors_of_trust + return @parsed_vectors_of_trust if defined?(@parsed_vectors_of_trust) - @parsed_vector_of_trust = begin + @parsed_vectors_of_trust = begin if vtr.is_a?(Array) && !vtr.empty? - Vot::Parser.new(vector_of_trust: vtr.first).parse + vtr.map { |vot| Vot::Parser.new(vector_of_trust: vot).parse } end rescue Vot::Parser::ParseException nil @@ -209,7 +192,7 @@ def validate_acr_values def validate_vtr return if vtr.blank? - return if parsed_vector_of_trust.present? + return if parsed_vectors_of_trust.present? errors.add( :vtr, t('openid_connect.authorization.errors.no_valid_vtr'), type: :no_valid_vtr @@ -336,7 +319,11 @@ def sp_defaults_to_identity_proofing? end def identity_proofing_requested? - ial == 2 + if parsed_vectors_of_trust.present? + parsed_vectors_of_trust.any?(&:identity_proofing?) + else + Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL[ial_values.sort.max] == 2 + end end def identity_proofing_service_provider? @@ -348,7 +335,7 @@ def ialmax_allowed_for_sp? end def ialmax_requested? - ial == 0 + Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL[ial_values.sort.max] == 0 end def highest_level_aal(aal_values) diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 6bc85b80884..6a7bf111812 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -529,7 +529,6 @@ function AcuantCapture( }); setImageCaptureText(''); - setIsCapturingEnvironment(true); } function onSelfieCaptureClosed() { diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx index 86dab1889a2..916f75a9ffd 100644 --- a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx @@ -21,6 +21,16 @@ function AcuantSelfieCaptureCanvas({ imageCaptureText, onSelfieCaptureClosed }) // The Acuant SDK script AcuantPassiveLiveness attaches to whatever element has // this id. It then uses that element as the root for the full screen selfie capture const acuantCaptureContainerId = 'acuant-face-capture-container'; + + // This solves a fairly nasty bug for screenreader users where the screenreader focus would jump away + // from the capture button (added by Acuant SDK) to the button in this component. Specifically we + // need to detect when Acuant actually hydrates in their capture screen and hide the button. + // See PR 10668 for more information. + const elementInShadow = document + ?.getElementById('acuant-face-capture-camera') + ?.shadowRoot?.getElementById('cameraContainer'); + const loadedAcuantCamera = !!elementInShadow; + return ( <> {!isReady && } @@ -31,9 +41,11 @@ function AcuantSelfieCaptureCanvas({ imageCaptureText, onSelfieCaptureClosed }) )}

- + {!loadedAcuantCamera && ( + + )} ); } diff --git a/app/javascript/packages/step-indicator/step-indicator-step.spec.tsx b/app/javascript/packages/step-indicator/step-indicator-step.spec.tsx index fcfce8a5151..785876b8105 100644 --- a/app/javascript/packages/step-indicator/step-indicator-step.spec.tsx +++ b/app/javascript/packages/step-indicator/step-indicator-step.spec.tsx @@ -14,7 +14,6 @@ describe('StepIndicatorStep', () => { expect(status).to.be.ok(); expect(step.classList.contains('step-indicator__step--current')).to.be.true(); expect(step.classList.contains('step-indicator__step--complete')).to.be.false(); - expect(status.classList.contains('step-indicator__step-subtitle')).to.be.false(); expect(status.classList.contains('usa-sr-only')).to.be.true(); }); }); @@ -31,7 +30,6 @@ describe('StepIndicatorStep', () => { expect(status).to.be.ok(); expect(step.classList.contains('step-indicator__step--current')).to.be.false(); expect(step.classList.contains('step-indicator__step--complete')).to.be.true(); - expect(status.classList.contains('step-indicator__step-subtitle')).to.be.false(); expect(status.classList.contains('usa-sr-only')).to.be.true(); }); }); @@ -50,7 +48,6 @@ describe('StepIndicatorStep', () => { expect(status).to.be.ok(); expect(step.classList.contains('step-indicator__step--current')).to.be.false(); expect(step.classList.contains('step-indicator__step--complete')).to.be.false(); - expect(status.classList.contains('step-indicator__step-subtitle')).to.be.false(); expect(status.classList.contains('usa-sr-only')).to.be.true(); }); }); diff --git a/app/jobs/reports/combined_invoice_supplement_report_v2.rb b/app/jobs/reports/combined_invoice_supplement_report_v2.rb index 915d23fa2de..2d34b71064f 100644 --- a/app/jobs/reports/combined_invoice_supplement_report_v2.rb +++ b/app/jobs/reports/combined_invoice_supplement_report_v2.rb @@ -44,14 +44,31 @@ def build_csv(iaas, partner_accounts) ) end + by_issuer_profile_age_results = iaas.flat_map do |iaa| + iaa.issuers.flat_map do |issuer| + Db::MonthlySpAuthCount::NewUniqueMonthlyUserCountsByPartner.call( + partner: issuer, # just a label + issuers: [issuer], + start_date: iaa.start_date, + end_date: iaa.end_date, + ) + end + end + combine_by_iaa_month( by_iaa_results: by_iaa_results, by_issuer_results: by_issuer_results, by_partner_results: by_partner_results, + by_issuer_profile_age_results: by_issuer_profile_age_results, ) end - def combine_by_iaa_month(by_iaa_results:, by_issuer_results:, by_partner_results:) + def combine_by_iaa_month( + by_iaa_results:, + by_issuer_results:, + by_partner_results:, + by_issuer_profile_age_results: + ) by_iaa_and_year_month = by_iaa_results.group_by do |result| [result[:key], result[:year_month]] end @@ -95,7 +112,13 @@ def combine_by_iaa_month(by_iaa_results:, by_issuer_results:, by_partner_results 'issuer_ial1_unique_users', 'issuer_ial2_unique_users', 'issuer_ial1_plus_2_unique_users', - 'issuer_ial2_new_unique_users', + 'issuer_ial2_new_unique_users_year1', + 'issuer_ial2_new_unique_users_year2', + 'issuer_ial2_new_unique_users_year3', + 'issuer_ial2_new_unique_users_year4', + 'issuer_ial2_new_unique_users_year5', + 'issuer_ial2_new_unique_users_year_greater_than_5', + 'issuer_ial2_new_unique_users_year_unknown', ] by_issuer_iaa_issuer_year_months.each do |iaa_key, issuer_year_months| issuer_year_months.each do |issuer, year_months_data| @@ -112,6 +135,11 @@ def combine_by_iaa_month(by_iaa_results:, by_issuer_results:, by_partner_results partner_results = by_partner_results.find do |result| result[:year_month] == year_month && result[:issuers]&.include?(issuer) end || {} + + issuer_profile_age_results = by_issuer_profile_age_results.find do |result| + result[:year_month] == year_month && result[:issuers]&.include?(issuer) + end || {} + csv << [ iaa_key, partner_results[:partner], @@ -142,7 +170,13 @@ def combine_by_iaa_month(by_iaa_results:, by_issuer_results:, by_partner_results (issuer_ial1_unique_users = extract(issuer_results, :unique_users, ial: 1)), (issuer_ial2_unique_users = extract(issuer_results, :unique_users, ial: 2)), issuer_ial1_unique_users + issuer_ial2_unique_users, - extract(issuer_results, :new_unique_users, ial: 2), + issuer_profile_age_results[:partner_ial2_new_unique_users_year1] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_users_year2] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_users_year3] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_users_year4] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_users_year5] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_users_year_greater_than_5] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_users_year_unknown] || 0, ] end end diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb index c53d6d720e2..b871d1a74be 100644 --- a/app/models/anonymous_user.rb +++ b/app/models/anonymous_user.rb @@ -48,4 +48,12 @@ def confirmed_at def locked_out? second_factor_locked_at.present? && !lockout_period_expired? end + + def identity_verified_with_biometric_comparison? + false + end + + def identity_verified? + false + end end diff --git a/app/models/backup_code_configuration.rb b/app/models/backup_code_configuration.rb index 1fc4e5e8600..4a744e885d2 100644 --- a/app/models/backup_code_configuration.rb +++ b/app/models/backup_code_configuration.rb @@ -35,18 +35,21 @@ class << self def find_with_code(code:, user_id:) return if code.blank? code = RandomPhrase.normalize(code) + user_salted_fingerprints = self.salted_fingerprints(code: code, user_id: user_id) + where(salted_code_fingerprint: user_salted_fingerprints).find_by(user_id: user_id) + end + + def salted_fingerprints(code:, user_id:) user_salt_costs = select(:code_salt, :code_cost). distinct. where(user_id: user_id). where.not(code_salt: nil).where.not(code_cost: nil). pluck(:code_salt, :code_cost) - salted_fingerprints = user_salt_costs.map do |salt, cost| + user_salt_costs.map do |salt, cost| scrypt_password_digest(password: code, salt: salt, cost: cost) end - - where(salted_code_fingerprint: salted_fingerprints).find_by(user_id: user_id) end def scrypt_password_digest(password:, salt:, cost:) diff --git a/app/models/federated_protocols/saml.rb b/app/models/federated_protocols/saml.rb index fdbc36d12c7..c0983064dde 100644 --- a/app/models/federated_protocols/saml.rb +++ b/app/models/federated_protocols/saml.rb @@ -27,7 +27,7 @@ def acr_values end def vtr - [request.requested_vtr_authn_context] if request.requested_vtr_authn_context.present? + request.requested_vtr_authn_contexts.presence end def requested_attributes diff --git a/app/models/user.rb b/app/models/user.rb index 8411ef8a682..2274e8dd6d9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -432,6 +432,11 @@ def new_device?(cookie_uuid:) !cookie_uuid || !devices.exists?(cookie_uuid:) end + def authenticated_device?(cookie_uuid:) + return false if cookie_uuid.blank? + devices.joins(:events).exists?(cookie_uuid:, events: { event_type: :sign_in_after_2fa }) + end + # Returns the number of times the user has signed in, corresponding to the `sign_in_before_2fa` # event. # diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index ab773c61559..23d3f02c607 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -45,9 +45,12 @@ def url_options private def vot_values - vot = JSON.parse(identity.vtr).first - parsed_vot = Vot::Parser.new(vector_of_trust: vot).parse - parsed_vot.expanded_component_values + AuthnContextResolver.new( + user: identity.user, + vtr: JSON.parse(identity.vtr), + service_provider: identity&.service_provider_record, + acr_values: nil, + ).resolve.expanded_component_values end def uuid_from_sp_identity(identity) @@ -140,6 +143,7 @@ def identity_proofing_requested_for_verified_user? def resolved_authn_context_result @resolved_authn_context_result ||= AuthnContextResolver.new( + user: identity.user, service_provider: identity&.service_provider_record, vtr: identity.vtr.presence && JSON.parse(identity.vtr), acr_values: identity.acr_values, diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 4016a43c446..f8f415cbe2a 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -128,6 +128,7 @@ def resolved_authn_context_result service_provider = ServiceProvider.find_by(issuer: sp) @resolved_authn_context_result = AuthnContextResolver.new( + user: user, service_provider:, vtr: session[:sp][:vtr], acr_values: session[:sp][:acr_values], diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index cd2b15c6291..2317b33810f 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -4918,7 +4918,7 @@ def saml_auth( # @param [Integer] requested_ial # @param [Array] authn_context # @param [String, nil] requested_aal_authn_context - # @param [String, nil] requested_vtr_authn_context + # @param [String, nil] requested_vtr_authn_contexts # @param [Boolean] force_authn # @param [Boolean] final_auth_request # @param [String] service_provider @@ -4928,7 +4928,7 @@ def saml_auth_request( requested_ial:, authn_context:, requested_aal_authn_context:, - requested_vtr_authn_context:, + requested_vtr_authn_contexts:, force_authn:, final_auth_request:, service_provider:, @@ -4941,7 +4941,7 @@ def saml_auth_request( requested_ial: requested_ial, authn_context: authn_context, requested_aal_authn_context: requested_aal_authn_context, - requested_vtr_authn_context: requested_vtr_authn_context, + requested_vtr_authn_contexts: requested_vtr_authn_contexts, force_authn: force_authn, final_auth_request: final_auth_request, service_provider: service_provider, diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index dd36578fc7b..c31ff8b81f3 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -38,7 +38,7 @@ def build add_all_emails(attrs) if bundle.include? :all_emails add_bundle(attrs) if should_add_proofed_attributes? add_verified_at(attrs) if bundle.include?(:verified_at) && ial2_service_provider? - if authn_request.requested_vtr_authn_context.present? + if authn_request.requested_vtr_authn_contexts.present? add_vot(attrs) else add_aal(attrs) @@ -71,6 +71,7 @@ def resolved_authn_context_result @resolved_authn_context_result ||= begin saml = FederatedProtocols::Saml.new(authn_request) AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: saml.vtr, acr_values: saml.acr_values, diff --git a/app/services/authn_context_resolver.rb b/app/services/authn_context_resolver.rb index 6495c2c2bec..bf7e5e1b55b 100644 --- a/app/services/authn_context_resolver.rb +++ b/app/services/authn_context_resolver.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class AuthnContextResolver - attr_reader :service_provider, :vtr, :acr_values + attr_reader :user, :service_provider, :vtr, :acr_values - def initialize(service_provider:, vtr:, acr_values:) + def initialize(user:, service_provider:, vtr:, acr_values:) + @user = user @service_provider = service_provider @vtr = vtr @acr_values = acr_values @@ -11,7 +12,7 @@ def initialize(service_provider:, vtr:, acr_values:) def resolve if vtr.present? - vot_parser_result + selected_vtr_parser_result_from_vtr_list else acr_result_with_sp_defaults end @@ -19,21 +20,52 @@ def resolve private - def vot_parser_result - @vot_result = Vot::Parser.new( - vector_of_trust: vtr&.first, - acr_values: acr_values, - ).parse + def selected_vtr_parser_result_from_vtr_list + if biometric_proofing_vot.present? && user&.identity_verified_with_biometric_comparison? + biometric_proofing_vot + elsif non_biometric_identity_proofing_vot.present? && user&.identity_verified? + non_biometric_identity_proofing_vot + elsif no_identity_proofing_vot.present? + no_identity_proofing_vot + else + parsed_vectors_of_trust.first + end + end + + def parsed_vectors_of_trust + @parsed_vectors_of_trust ||= vtr.map do |vot| + Vot::Parser.new(vector_of_trust: vot).parse + end + end + + def biometric_proofing_vot + parsed_vectors_of_trust.find(&:biometric_comparison?) + end + + def non_biometric_identity_proofing_vot + parsed_vectors_of_trust.find do |vot_parser_result| + vot_parser_result.identity_proofing? && !vot_parser_result.biometric_comparison? + end + end + + def no_identity_proofing_vot + parsed_vectors_of_trust.find do |vot_parser_result| + !vot_parser_result.identity_proofing? + end end def acr_result_with_sp_defaults result_with_sp_aal_defaults( result_with_sp_ial_defaults( - vot_parser_result, + acr_result_without_sp_defaults, ), ) end + def acr_result_without_sp_defaults + @acr_result_without_sp_defaults ||= Vot::Parser.new(acr_values: acr_values).parse + end + def result_with_sp_aal_defaults(result) if acr_aal_component_values.any? result @@ -57,14 +89,14 @@ def result_with_sp_ial_defaults(result) end def acr_aal_component_values - vot_parser_result.component_values.filter do |component_value| + acr_result_without_sp_defaults.component_values.filter do |component_value| component_value.name.include?('aal') || component_value.name == Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF end end def acr_ial_component_values - vot_parser_result.component_values.filter do |component_value| + acr_result_without_sp_defaults.component_values.filter do |component_value| component_value.name.include?('ial') || component_value.name.include?('loa') end end diff --git a/app/services/backup_code_generator.rb b/app/services/backup_code_generator.rb index 575d3f9e019..e7b0d0ca615 100644 --- a/app/services/backup_code_generator.rb +++ b/app/services/backup_code_generator.rb @@ -12,41 +12,46 @@ def initialize(user, num_words: BackupCodeConfiguration::NUM_WORDS) @user = user end - # @return [Array] - def generate - delete_existing_codes - generate_new_codes - end + def delete_and_regenerate(salt: SecureRandom.hex(32)) + codes = generate_new_codes + + BackupCodeConfiguration.transaction do + @user.backup_code_configurations.destroy_all + codes.each { |code| save_code(code: code, salt: salt) } + end - # @return [Array] - def create - @user.save - save(generate) + codes end # @return [Boolean] def verify(plaintext_code) - if_valid_consume_code_return_config(plaintext_code).present? + if_valid_consume_code_return_config_created_at(plaintext_code).present? end # @return [BackupCodeConfiguration, nil] - def if_valid_consume_code_return_config(plaintext_code) + def if_valid_consume_code_return_config_created_at(plaintext_code) return unless plaintext_code.present? backup_code = RandomPhrase.normalize(plaintext_code) - config = BackupCodeConfiguration.find_with_code(code: backup_code, user_id: @user.id) - return unless code_usable?(config) - config.update!(used_at: Time.zone.now) - config - end - - def delete_existing_codes - @user.backup_code_configurations.destroy_all - end + return nil unless backup_code + + salted_fingerprints = + BackupCodeConfiguration.salted_fingerprints(code: backup_code, user_id: @user.id) + + query_result = BackupCodeConfiguration.transaction do + sql = <<~SQL + UPDATE backup_code_configurations + SET + used_at = NOW() + WHERE user_id = ? AND salted_code_fingerprint IN (?) AND used_at IS NULL + RETURNING created_at; + SQL + query = BackupCodeConfiguration.sanitize_sql_array( + [sql, @user.id, salted_fingerprints], + ) + BackupCodeConfiguration.connection.execute(query).first + end - # @return [Array] - def save(codes, salt: SecureRandom.hex(32)) - delete_existing_codes - codes.each { |code| save_code(code: code, salt: salt) } + query_result['created_at'] if query_result end private diff --git a/app/services/doc_auth/error_generator.rb b/app/services/doc_auth/error_generator.rb index 2fb20cb29c1..51b5033c1ac 100644 --- a/app/services/doc_auth/error_generator.rb +++ b/app/services/doc_auth/error_generator.rb @@ -115,13 +115,12 @@ def get_doc_auth_errors(response_info, known_error_count) return if known_error_count < 1 doc_auth_error_messages = get_doc_auth_error_messages(response_info) - liveness_enabled = response_info[:liveness_enabled] if known_error_count == 1 process_single_doc_auth_error(doc_auth_error_messages) else # Simplify multiple errors into a single error for the user - consolidate_multiple_doc_auth_errors(doc_auth_error_messages, liveness_enabled) + consolidate_multiple_doc_auth_errors(doc_auth_error_messages) end end @@ -147,20 +146,20 @@ def process_single_doc_auth_error(alert_errors) ErrorResult.new(error, side) end - def consolidate_multiple_doc_auth_errors(alert_errors, liveness_enabled) + def consolidate_multiple_doc_auth_errors(alert_errors) error_fields = alert_errors.keys if error_fields.length == 1 side = error_fields.first case side when ErrorGenerator::ID - error = ErrorGenerator.general_error(liveness_enabled) + error = Errors::GENERAL_ERROR when ErrorGenerator::FRONT error = Errors::MULTIPLE_FRONT_ID_FAILURES when ErrorGenerator::BACK error = Errors::MULTIPLE_BACK_ID_FAILURES end elsif error_fields.length > 1 - error = ErrorGenerator.general_error(liveness_enabled) + error = Errors::GENERAL_ERROR side = ErrorGenerator::ID end ErrorResult.new(error, side) @@ -239,8 +238,7 @@ def process_unknown_error(response_info) response_info: response_info, ) - liveness_enabled = response_info[:liveness_enabled] - error = ErrorGenerator.general_error(liveness_enabled) + error = Errors::GENERAL_ERROR side = ErrorGenerator::ID ErrorResult.new(error, side) end @@ -339,12 +337,8 @@ def generate_doc_auth_errors(response_info) unknown_error_handler.handle(response_info).to_h end - def self.general_error(_liveness_enabled) - Errors::GENERAL_ERROR - end - - def self.wrapped_general_error(liveness_enabled) - { general: [ErrorGenerator.general_error(liveness_enabled)], hints: true } + def self.wrapped_general_error + { general: [Errors::GENERAL_ERROR], hints: true } end private 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 80989ff8282..a8f22585d94 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 @@ -67,7 +67,7 @@ def error_messages if with_authentication_result? ErrorGenerator.new(config).generate_doc_auth_errors(response_info) elsif true_id_product.present? - ErrorGenerator.wrapped_general_error(@liveness_checking_enabled) + ErrorGenerator.wrapped_general_error else { network: true } # return a generic technical difficulties error to user end diff --git a/app/services/id_token_builder.rb b/app/services/id_token_builder.rb index 8a61ac5b8ca..ed8890ea9ac 100644 --- a/app/services/id_token_builder.rb +++ b/app/services/id_token_builder.rb @@ -93,6 +93,7 @@ def determine_ial_max_acr def resolved_authn_context_result @resolved_authn_context_result ||= AuthnContextResolver.new( + user: identity.user, service_provider: identity.service_provider_record, vtr: parsed_vtr_value, acr_values: identity.acr_values, diff --git a/app/services/proofing/aamva/applicant.rb b/app/services/proofing/aamva/applicant.rb index a567329ca1e..d704cfe8708 100644 --- a/app/services/proofing/aamva/applicant.rb +++ b/app/services/proofing/aamva/applicant.rb @@ -21,6 +21,8 @@ module Aamva :state_id_number, :state_id_jurisdiction, :state_id_type, + :state_id_issued, + :state_id_expiration, keyword_init: true, ).freeze @@ -64,6 +66,8 @@ def self.from_proofer_applicant(applicant) state_id_number: applicant.dig(:state_id_number)&.gsub(/[^\w\d]/, ''), state_id_jurisdiction: applicant[:state_id_jurisdiction], state_id_type: applicant[:state_id_type], + state_id_issued: applicant[:state_id_issued], + state_id_expiration: applicant[:state_id_expiration], ) end end.freeze diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index c73841282d8..48616af80d0 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -21,6 +21,18 @@ class Proofer ], ).freeze + ADDRESS_ATTRIBUTES = [ + :address1, + :address2, + :city, + :state, + :zipcode, + ].to_set.freeze + + OPTIONAL_ADDRESS_ATTRIBUTES = [:address2].freeze + + REQUIRED_ADDRESS_ATTRIBUTES = (ADDRESS_ATTRIBUTES - OPTIONAL_ADDRESS_ATTRIBUTES).freeze + attr_reader :config # Instance methods @@ -31,6 +43,7 @@ def initialize(config) def proof(applicant) aamva_applicant = Aamva::Applicant.from_proofer_applicant(OpenStruct.new(applicant)) + response = Aamva::VerificationClient.new( config, ).send_verification_request( @@ -55,6 +68,7 @@ def build_result_from_response(verification_response) exception: nil, vendor_name: 'aamva:state_id', transaction_id: verification_response.transaction_locator_id, + requested_attributes: requested_attributes(verification_response).index_with(1), verified_attributes: verified_attributes(verification_response), ) end @@ -73,30 +87,30 @@ def parse_verification_errors(verification_response) errors end - def verified_attributes(verification_response) - attributes = Set.new - results = verification_response.verification_results - - attributes.add :address if address_verified?(results) + def requested_attributes(verification_response) + attributes = verification_response. + verification_results.filter { |_, verified| !verified.nil? }. + keys. + to_set - results.delete :address1 - results.delete :address2 - results.delete :city - results.delete :state - results.delete :zipcode + normalize_address_attributes(attributes) + end - results.each do |attribute, verified| - attributes.add attribute if verified - end + def verified_attributes(verification_response) + attributes = verification_response. + verification_results.filter { |_, verified| verified }. + keys. + to_set - attributes + normalize_address_attributes(attributes) end - def address_verified?(results) - results[:address1] && - results[:city] && - results[:state] && - results[:zipcode] + def normalize_address_attributes(attribute_set) + all_present = REQUIRED_ADDRESS_ATTRIBUTES & attribute_set == REQUIRED_ADDRESS_ATTRIBUTES + + (attribute_set - ADDRESS_ATTRIBUTES).tap do |result| + result.add(:address) if all_present + end end def send_to_new_relic(result) diff --git a/app/services/proofing/aamva/request/templates/verify.xml.erb b/app/services/proofing/aamva/request/templates/verify.xml.erb index c9ab21d99b6..d2f71e6b7e5 100644 --- a/app/services/proofing/aamva/request/templates/verify.xml.erb +++ b/app/services/proofing/aamva/request/templates/verify.xml.erb @@ -1,37 +1,41 @@ - + http://aamva.org/dldv/wsdl/2.1/IDLDVService21/VerifyDriverLicenseData - - + + <%= auth_token %> - - - - - + + + + + <%= transaction_locator_id %> - - GSA - - - - - - - - - - - - - - - - - - - + + GSA + + + + + + + + + + + + + + + + + + + diff --git a/app/services/proofing/aamva/request/verification_request.rb b/app/services/proofing/aamva/request/verification_request.rb index df38c8b2d3f..ecff76af4a4 100644 --- a/app/services/proofing/aamva/request/verification_request.rb +++ b/app/services/proofing/aamva/request/verification_request.rb @@ -62,34 +62,43 @@ def add_user_provided_data_to_body user_provided_data_map.each do |xpath, data| REXML::XPath.first(document, xpath).add_text(data) end - add_street_address_line_2_to_rexml_document(document) if applicant.address2.present? + + add_optional_element( + 'nc:AddressDeliveryPointText', + value: applicant.address2, + document:, + after: '//aa:Address/nc:AddressDeliveryPointText', + ) + + add_optional_element( + 'aa:DriverLicenseIssueDate', + value: applicant.state_id_data.state_id_issued, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_optional_element( + 'aa:DriverLicenseExpirationDate', + value: applicant.state_id_data.state_id_expiration, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + @body = document.to_s end - def add_street_address_line_2_to_rexml_document(document) - old_address_node = document.delete_element('//ns1:Address') - new_address_node = old_address_node.clone - old_address_node.children.each do |child_node| - next unless child_node.node_type == :element + def add_optional_element(name, value:, document:, inside: nil, after: nil) + return if value.blank? - new_element = child_node.clone - new_element.add_text(child_node.text) - new_address_node.add_element(new_element) + el = REXML::Element.new(name) + el.text = value - if child_node.name == 'AddressDeliveryPointText' - new_address_node.add_element(address_line_2_element) - end + if inside + REXML::XPath.first(document, inside).add_element(el) + elsif after + sibling = REXML::XPath.first(document, after) + sibling.parent.insert_after(sibling, el) end - REXML::XPath.first( - document, - '//ns:verifyDriverLicenseDataRequest', - ).add_element(new_address_node) - end - - def address_line_2_element - element = REXML::Element.new('ns2:AddressDeliveryPointText') - element.add_text(applicant.address2) - element end def build_request_body @@ -133,15 +142,15 @@ def transaction_locator_id def user_provided_data_map { - '//ns2:IdentificationID' => state_id_number, - '//ns1:MessageDestinationId' => message_destination_id, - '//ns2:PersonGivenName' => applicant.first_name, - '//ns2:PersonSurName' => applicant.last_name, - '//ns1:PersonBirthDate' => applicant.dob, - '//ns2:AddressDeliveryPointText' => applicant.address1, - '//ns2:LocationCityName' => applicant.city, - '//ns2:LocationStateUsPostalServiceCode' => applicant.state, - '//ns2:LocationPostalCode' => applicant.zipcode, + '//nc:IdentificationID' => state_id_number, + '//aa:MessageDestinationId' => message_destination_id, + '//nc:PersonGivenName' => applicant.first_name, + '//nc:PersonSurName' => applicant.last_name, + '//aa:PersonBirthDate' => applicant.dob, + '//nc:AddressDeliveryPointText' => applicant.address1, + '//nc:LocationCityName' => applicant.city, + '//nc:LocationStateUsPostalServiceCode' => applicant.state, + '//nc:LocationPostalCode' => applicant.zipcode, } end diff --git a/app/services/proofing/aamva/response/verification_response.rb b/app/services/proofing/aamva/response/verification_response.rb index b26c6e20f69..adcbdadfb7b 100644 --- a/app/services/proofing/aamva/response/verification_response.rb +++ b/app/services/proofing/aamva/response/verification_response.rb @@ -8,6 +8,8 @@ module Aamva module Response class VerificationResponse VERIFICATION_ATTRIBUTES_MAP = { + 'DriverLicenseExpirationDateMatchIndicator' => :state_id_expiration, + 'DriverLicenseIssueDateMatchIndicator' => :state_id_issued, 'DriverLicenseNumberMatchIndicator' => :state_id_number, 'DocumentCategoryMatchIndicator' => :state_id_type, 'PersonBirthDateMatchIndicator' => :dob, @@ -62,6 +64,7 @@ def success? REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute| return false unless verification_results[verification_attribute] end + true end diff --git a/app/services/proofing/state_id_result.rb b/app/services/proofing/state_id_result.rb index 459603b67d5..02b2b0416ba 100644 --- a/app/services/proofing/state_id_result.rb +++ b/app/services/proofing/state_id_result.rb @@ -11,6 +11,7 @@ class StateIdResult :success, :vendor_name, :transaction_id, + :requested_attributes, :verified_attributes def initialize( @@ -19,6 +20,7 @@ def initialize( exception: nil, vendor_name: nil, transaction_id: '', + requested_attributes: {}, verified_attributes: [] ) @success = success @@ -26,6 +28,7 @@ def initialize( @exception = exception @vendor_name = vendor_name @transaction_id = transaction_id + @requested_attributes = requested_attributes @verified_attributes = verified_attributes end @@ -59,6 +62,7 @@ def to_h errors: errors, exception: exception, mva_exception: mva_exception?, + requested_attributes: requested_attributes, timed_out: timed_out?, transaction_id: transaction_id, vendor_name: vendor_name, diff --git a/app/services/saml_request_validator.rb b/app/services/saml_request_validator.rb index cddcd59198b..2b21aadc0bc 100644 --- a/app/services/saml_request_validator.rb +++ b/app/services/saml_request_validator.rb @@ -22,14 +22,8 @@ def call(service_provider:, authn_context:, nameid_format:, authn_context_compar FormResponse.new(success: valid?, errors: errors, extra: extra_analytics_attributes) end - def parsed_vector_of_trust - return @parsed_vector_of_trust if defined?(@parsed_vector_of_trust) - - @parsed_vector_of_trust = begin - Vot::Parser.new(vector_of_trust: vtr.first).parse if !vtr.blank? - rescue Vot::Parser::ParseException - nil - end + def biometric_comparison_requested? + !!parsed_vectors_of_trust&.any?(&:biometric_comparison?) end private @@ -45,6 +39,18 @@ def extra_analytics_attributes } end + def parsed_vectors_of_trust + return @parsed_vectors_of_trust if defined?(@parsed_vectors_of_trust) + + @parsed_vectors_of_trust = begin + if vtr.is_a?(Array) && !vtr.empty? + vtr.map { |vot| Vot::Parser.new(vector_of_trust: vot).parse } + end + rescue Vot::Parser::ParseException + nil + end + end + # This checks that the SP matches something in the database # SamlIdpAuthConcern#check_sp_active checks that it's currently active def authorized_service_provider @@ -65,7 +71,7 @@ def authorized_authn_context end def parsable_vtr - if !vtr.blank? && parsed_vector_of_trust.blank? + if !vtr.blank? && parsed_vectors_of_trust.blank? errors.add(:authn_context, :unauthorized_authn_context, type: :unauthorized_authn_context) end end @@ -95,7 +101,7 @@ def step_up_comparison? end def identity_proofing_requested? - return true if parsed_vector_of_trust&.identity_proofing? + return true if parsed_vectors_of_trust&.any?(&:identity_proofing?) authn_context.each do |classref| return true if Saml::Idp::Constants::IAL2_AUTHN_CONTEXTS.include?(classref) diff --git a/app/services/user_alerts/alert_user_about_new_device.rb b/app/services/user_alerts/alert_user_about_new_device.rb index 30037eebc39..28d70f4172f 100644 --- a/app/services/user_alerts/alert_user_about_new_device.rb +++ b/app/services/user_alerts/alert_user_about_new_device.rb @@ -3,26 +3,28 @@ module UserAlerts class AlertUserAboutNewDevice def self.call(event:, device:, disavowal_token:) - if IdentityConfig.store.feature_new_device_alert_aggregation_enabled - event.user.sign_in_new_device_at ||= event.created_at - event.user.save - else - device_decorator = DeviceDecorator.new(device) - login_location = device_decorator.last_sign_in_location_and_ip - device_name = device_decorator.nice_name + return if IdentityConfig.store.feature_new_device_alert_aggregation_enabled + device_decorator = DeviceDecorator.new(device) + login_location = device_decorator.last_sign_in_location_and_ip + device_name = device_decorator.nice_name - event.user.confirmed_email_addresses.each do |email_address| - UserMailer.with(user: event.user, email_address: email_address).new_device_sign_in( - date: device.last_used_at.in_time_zone('Eastern Time (US & Canada)'). - strftime('%B %-d, %Y %H:%M Eastern Time'), - location: login_location, - device_name: device_name, - disavowal_token: disavowal_token, - ).deliver_now_or_later - end + event.user.confirmed_email_addresses.each do |email_address| + UserMailer.with(user: event.user, email_address: email_address).new_device_sign_in( + date: device.last_used_at.in_time_zone('Eastern Time (US & Canada)'). + strftime('%B %-d, %Y %H:%M Eastern Time'), + location: login_location, + device_name: device_name, + disavowal_token: disavowal_token, + ).deliver_now_or_later end end + def self.schedule_alert(event:) + return if !IdentityConfig.store.feature_new_device_alert_aggregation_enabled || + event.user.sign_in_new_device_at.present? + event.user.update(sign_in_new_device_at: event.created_at) + end + def self.send_alert(user:, disavowal_event:, disavowal_token:) return false unless user.sign_in_new_device_at diff --git a/app/services/user_seeder.rb b/app/services/user_seeder.rb index be4778cf6f4..98c7d3fa6f0 100644 --- a/app/services/user_seeder.rb +++ b/app/services/user_seeder.rb @@ -99,10 +99,7 @@ def setup_user(user:, ee:) EmailAddress.create!(user: user, email: ee.decrypted, confirmed_at: Time.zone.now) user.reset_password(PASSWORD, PASSWORD) Event.create(user_id: user.id, event_type: :account_created) - generator = BackupCodeGenerator.new(user) - generator.generate.tap do |codes| - generator.save(codes) - end + BackupCodeGenerator.new(user).delete_and_regenerate end def create_profile(user:, row:) diff --git a/app/services/vot/parser.rb b/app/services/vot/parser.rb index 80e7cbee65d..004d77c3a6f 100644 --- a/app/services/vot/parser.rb +++ b/app/services/vot/parser.rb @@ -12,6 +12,7 @@ class ParseException < StandardError; end :identity_proofing?, :biometric_comparison?, :ialmax?, + :enhanced_ipp?, ) do def self.no_sp_result self.new( @@ -22,6 +23,7 @@ def self.no_sp_result identity_proofing?: false, biometric_comparison?: false, ialmax?: false, + enhanced_ipp?: false, ) end @@ -86,6 +88,7 @@ def expand_components_with_initial_components(initial_components) identity_proofing?: requirement_list.include?(:identity_proofing), biometric_comparison?: requirement_list.include?(:biometric_comparison), ialmax?: requirement_list.include?(:ialmax), + enhanced_ipp?: requirement_list.include?(:enhanced_ipp), ) end diff --git a/app/services/vot/supported_component_values.rb b/app/services/vot/supported_component_values.rb index d946e355945..a49f23a7d45 100644 --- a/app/services/vot/supported_component_values.rb +++ b/app/services/vot/supported_component_values.rb @@ -38,6 +38,12 @@ module SupportedComponentValues implied_component_values: [P1], requirements: [:biometric_comparison], ).freeze + Pe = ComponentValue.new( + name: 'Pe', + description: 'Enhanced In Person Proofing is required', + implied_component_values: [P1], + requirements: [:enhanced_ipp], + ).freeze NAME_HASH = constants.map do |constant| component_value = const_get(constant) diff --git a/app/views/idv/document_capture/show.html.erb b/app/views/idv/document_capture/show.html.erb index 781ae3d6e5a..79eeba90896 100644 --- a/app/views/idv/document_capture/show.html.erb +++ b/app/views/idv/document_capture/show.html.erb @@ -9,7 +9,7 @@ acuant_version: acuant_version, opted_in_to_in_person_proofing: opted_in_to_in_person_proofing, skip_doc_auth: skip_doc_auth, - skip_doc_auth_from_how_to_verify: false, + skip_doc_auth_from_how_to_verify: skip_doc_auth_from_how_to_verify, skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, doc_auth_selfie_capture: doc_auth_selfie_capture, ) %> diff --git a/app/views/idv/hybrid_handoff/show.html.erb b/app/views/idv/hybrid_handoff/show.html.erb index 36442678d2a..4110740bd43 100644 --- a/app/views/idv/hybrid_handoff/show.html.erb +++ b/app/views/idv/hybrid_handoff/show.html.erb @@ -90,17 +90,17 @@ ) %> <%= f.submit t('forms.buttons.send_link') %> <% end %> - - <% if @direct_ipp_with_selfie_enabled %> -
-

- <%= t('doc_auth.info.hybrid_handoff_ipp_html') %> -

-

- <%= link_to t('in_person_proofing.headings.prepare'), idv_document_capture_path(step: :hybrid_handoff) %> -

+ <% if @direct_ipp_with_selfie_enabled %> +
+

+ <%= t('doc_auth.info.hybrid_handoff_ipp_html') %> +

+

+ <%= link_to t('in_person_proofing.headings.prepare'), idv_document_capture_path(step: :hybrid_handoff) %> +

+
+ <% end %>
- <% end %> <% end %> diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index b427ae9d82b..597c89561db 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -38,7 +38,7 @@ doc_auth_selfie_capture: FeatureManagement.idv_allow_selfie_check? && doc_auth_selfie_capture, doc_auth_selfie_desktop_test_mode: IdentityConfig.store.doc_auth_selfie_desktop_test_mode, skip_doc_auth: skip_doc_auth, - skip_doc_auth_from_how_to_verify: false, + skip_doc_auth_from_how_to_verify: skip_doc_auth_from_how_to_verify, skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, how_to_verify_url: idv_how_to_verify_url, previous_step_url: @previous_step_url, diff --git a/app/views/shared/_banner.html.erb b/app/views/shared/_banner.html.erb index b8e8d57c21f..eb51448c7ef 100644 --- a/app/views/shared/_banner.html.erb +++ b/app/views/shared/_banner.html.erb @@ -1,6 +1,6 @@
<%= render 'shared/no_pii_banner' if FeatureManagement.show_no_pii_banner? %> -
+
diff --git a/app/views/users/backup_code_setup/index.html.erb b/app/views/users/backup_code_setup/create.html.erb similarity index 100% rename from app/views/users/backup_code_setup/index.html.erb rename to app/views/users/backup_code_setup/create.html.erb diff --git a/app/views/users/backup_code_setup/edit.html.erb b/app/views/users/backup_code_setup/edit.html.erb index 0d872caed14..580c811f447 100644 --- a/app/views/users/backup_code_setup/edit.html.erb +++ b/app/views/users/backup_code_setup/edit.html.erb @@ -7,6 +7,7 @@ <%= render ButtonComponent.new( url: backup_code_setup_path, + method: IdentityConfig.store.backup_code_confirm_setup_screen_enabled ? :post : :get, big: true, wide: true, class: 'margin-top-3 margin-bottom-2', diff --git a/app/views/users/backup_code_setup/new.html.erb b/app/views/users/backup_code_setup/new.html.erb new file mode 100644 index 00000000000..524e8e0e939 --- /dev/null +++ b/app/views/users/backup_code_setup/new.html.erb @@ -0,0 +1,22 @@ +<% self.title = t('two_factor_authentication.confirm_backup_code_setup_title') %> + +<%= render PageHeadingComponent.new.with_content(t('two_factor_authentication.confirm_backup_code_setup_title')) %> + +<% t('two_factor_authentication.confirm_backup_code_setup_content_html').each do |desc_p| %> +

<%= desc_p %>

+<% end %> + +<%= render ButtonComponent.new( + url: backup_code_setup_path, + method: :post, + big: true, + wide: true, + class: 'display-block margin-top-5 margin-bottom-2', + ).with_content(t('forms.buttons.continue')) %> + +<%= render ButtonComponent.new( + url: session[:account_redirect_path] || account_path, + big: true, + wide: true, + outline: true, + ).with_content(t('links.cancel')) %> diff --git a/config/application.yml.default b/config/application.yml.default index 772c6068aee..bbed146510e 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -58,6 +58,7 @@ aws_kms_multi_region_key_id: alias/login-dot-gov-keymaker-multi-region aws_kms_session_key_id: alias/login-dot-gov-test-keymaker aws_logo_bucket: '' aws_region: 'us-west-2' +backup_code_confirm_setup_screen_enabled: true backup_code_cost: '2000$8$1$' broken_personal_key_window_start: '2021-07-29T00:00:00Z' broken_personal_key_window_finish: '2021-09-22T00:00:00Z' @@ -454,6 +455,7 @@ production: attribute_encryption_key_queue: '[]' available_locales: 'en,es,fr' aws_logo_bucket: '' + backup_code_confirm_setup_screen_enabled: false dashboard_api_token: '' dashboard_url: https://dashboard.demo.login.gov database_host: '' diff --git a/config/locales/en.yml b/config/locales/en.yml index 4896863606a..0cfa2cba1aa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1580,6 +1580,10 @@ two_factor_authentication.backup_code_prompt: You can use this backup code once. two_factor_authentication.backup_codes.instructions: If you don’t have access to another device, keep your backup codes safe. If you lose your backup codes, you won’t be able to sign into %{app_name}. two_factor_authentication.backup_codes.warning_html: 'You’ve only set up backup codes on your account. If you have access to another device, such as a phone, protect your account with another authentication method.' two_factor_authentication.choose_another_option: '‹ Choose another authentication method' +two_factor_authentication.confirm_backup_code_setup_content_html: + - 'Backup codes are the least preferred authentication method because the codes can easily be lost. Try a safer option, like an authentication application or a security key.' + - We’ll give you 10 codes that you can download, print, copy or write down. You’ll enter one code every time you sign in. +two_factor_authentication.confirm_backup_code_setup_title: Are you sure you want to use backup codes? two_factor_authentication.form_legend: Choose your authentication methods two_factor_authentication.header_text: Enter your one-time code two_factor_authentication.important_alert_icon: important alert icon diff --git a/config/locales/es.yml b/config/locales/es.yml index 52ecfa67d3f..fc10dbeff5e 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -587,9 +587,9 @@ doc_auth.info.exit.with_sp: Salir de %{app_name} y volver a %{sp_name} doc_auth.info.exit.without_sp: Salga de la verificación de identidad y vaya a la página de su cuenta doc_auth.info.getting_started_html: '%{sp_name} necesita asegurarse de que se trata de usted y no de alguien que se hace pasar por usted. %{link_html}' doc_auth.info.getting_started_learn_more: Obtenga más información sobre la verificación de su identidad -doc_auth.info.how_to_verify: Tiene la opción de verificar su identidad en línea o en persona en una oficina de correos participante. +doc_auth.info.how_to_verify: Tiene la opción de verificar su identidad en línea, o en persona en una oficina de correos participante. doc_auth.info.how_to_verify_selfie: Tiene la opción de verificar su identidad en línea con su teléfono, o en persona en una oficina de correos participante. -doc_auth.info.how_to_verify_troubleshooting_options_header: '¿Desea obtener más información sobre cómo verificar su identidad?' +doc_auth.info.how_to_verify_troubleshooting_options_header: ¿Desea obtener más información sobre cómo verificar su identidad? doc_auth.info.hybrid_handoff: Recopilaremos información sobre usted leyendo su identificación emitida por el estado. doc_auth.info.hybrid_handoff_ipp_html: '¿No tiene un teléfono móvil? Puede verificar su identidad en una oficina de correos de los Estados Unidos.' doc_auth.info.image_loaded: Imagen cargada @@ -605,7 +605,7 @@ doc_auth.info.link_sent_complete_no_polling: Cuando termine, haga clic en “Con doc_auth.info.link_sent_complete_polling: El siguiente paso se cargará automáticamente. doc_auth.info.no_ssn: Debe tener un número de Seguro Social para finalizar la verificación de su identidad. doc_auth.info.review_examples_of_photos: Vea ejemplos de cómo tomar fotos nítidas de su identificación. -doc_auth.info.secure_account: Cifraremos su cuenta cuando vuelva a introducir su contraseña. La encriptación significa que sus datos están protegidos y solo usted podrá acceder o modificar su información. +doc_auth.info.secure_account: Cifraremos su cuenta cuando vuelva a ingresar su contraseña. Con el cifrado, sus datos están protegidos y solo usted puede acceder a su información o modificarla. doc_auth.info.selfie_capture_content: Revisaremos que usted sea la persona que figura en su identificación. doc_auth.info.selfie_capture_status.face_close_to_border: Demasiado cerca del marco doc_auth.info.selfie_capture_status.face_not_found: No se detectó el rostro @@ -618,7 +618,7 @@ doc_auth.info.upload_from_computer: '¿No tiene un teléfono? Cargue fotos de su doc_auth.info.upload_from_phone: No tendrá que volver a iniciar sesión y volverá a esta computadora después de tomar las fotos. Su teléfono móvil debe tener una cámara y un navegador web. doc_auth.info.verify_at_post_office_description: Esta opción es mejor si no tiene un teléfono para tomar fotografías de su identificación. doc_auth.info.verify_at_post_office_description_selfie: Elija esta opción si no tiene un teléfono para tomar fotografías. -doc_auth.info.verify_at_post_office_instruction: Ingresará la información de su identificación en línea y verificará su identidad en persona en una oficina de correos participante. +doc_auth.info.verify_at_post_office_instruction: Ingresará la información de su identificación en línea, y verificará su identidad en persona en una oficina de correos participante. doc_auth.info.verify_at_post_office_instruction_selfie: Ingresará la información de su identificación en línea, y verificará su identidad en persona en una oficina de correos participante. doc_auth.info.verify_at_post_office_link_text: Obtenga más información sobre la verificación en persona doc_auth.info.verify_identity: Le pediremos su identificación, número de teléfono y otros datos personales para verificar su identidad comparándola con los registros públicos. @@ -1579,6 +1579,10 @@ two_factor_authentication.backup_code_prompt: Puede utilizar este código de rec two_factor_authentication.backup_codes.instructions: Si no tiene acceso a otro dispositivo, proteja sus códigos de recuperación. Si los pierde, no podrá iniciar sesión en %{app_name}. two_factor_authentication.backup_codes.warning_html: 'Solo ha configurado códigos de recuperación en su cuenta. Si tiene acceso a otro dispositivo, como un teléfono, proteja su cuenta con otro método de autenticación.' two_factor_authentication.choose_another_option: '‹ Elija otro método de autenticación' +two_factor_authentication.confirm_backup_code_setup_content_html: + - 'Los códigos de recuperación son el método de autenticación menos utilizado, ya que se pueden perder con facilidad. Pruebe una opción más segura, como una aplicación de autenticación o una clave de seguridad.' + - Le enviaremos 10 códigos que puede descargar, imprimir, copiar o anotar. Ingresará un código cada vez que inicie sesión. +two_factor_authentication.confirm_backup_code_setup_title: ¿Está seguro de que desea usar códigos de recuperación? two_factor_authentication.form_legend: Elija sus métodos de autenticación two_factor_authentication.header_text: Introduzca su código de un solo uso two_factor_authentication.important_alert_icon: icono importante de alerta @@ -1809,7 +1813,7 @@ user_mailer.letter_reminder_14_days.subject: Termine de verificar su identidad user_mailer.letter_reminder.info_html: La carta que recibirá próximamente contiene un código de verificación que nos ayudará a verificar su dirección. Para completar el proceso de verificación de identidad, inicie sesión en %{link_html} e ingrese el código de verificación. user_mailer.letter_reminder.subject: Enviamos una carta a la dirección de su expediente user_mailer.new_device_sign_in_after_2fa.authentication_methods: métodos de autenticación -user_mailer.new_device_sign_in_after_2fa.info_p1: Su correo electrónico y su contraseña de %{app_name} se usaron para iniciar sesión y para la autenticación desde un nuevo dispositivo. +user_mailer.new_device_sign_in_after_2fa.info_p1: Su correo electrónico y contraseña de %{app_name} se usaron para iniciar sesión y hacer la autenticación en un dispositivo nuevo. user_mailer.new_device_sign_in_after_2fa.info_p2: Si reconoce esta actividad, no tiene que hacer nada. user_mailer.new_device_sign_in_after_2fa.info_p3_html: Si no fue usted, %{reset_password_link_html} y cambie sus %{authentication_methods_link_html} inmediatamente. user_mailer.new_device_sign_in_after_2fa.reset_password: restablezca la contraseña @@ -1817,13 +1821,13 @@ user_mailer.new_device_sign_in_after_2fa.subject: Nuevo inicio de sesión y aute user_mailer.new_device_sign_in_attempts.events.sign_in_after_2fa: Autenticado user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa: Inicia sesión con contraseña user_mailer.new_device_sign_in_attempts.events.sign_in_unsuccessful_2fa: Error al autenticar -user_mailer.new_device_sign_in_attempts.new_sign_in_from: Nuevo inicio de sesión potencialmente ubicado en %{location} -user_mailer.new_device_sign_in_before_2fa.info_p1_html.one: Su correo electrónico y su contraseña de %{app_name} se usaron para ingresar desde un nuevo dispositivo, pero la autenticación dio error. +user_mailer.new_device_sign_in_attempts.new_sign_in_from: El nuevo inicio de sesión se encuentra potencialmente en %{location} +user_mailer.new_device_sign_in_before_2fa.info_p1_html.one: Su correo electrónico y su contraseña de %{app_name} se usaron para ingresar desde un nuevo dispositivo, pero no se logró la autenticación. user_mailer.new_device_sign_in_before_2fa.info_p1_html.other: Su correo electrónico y su contraseña de %{app_name} se usaron para ingresar desde un nuevo dispositivo, pero error al autenticar %{count} veces. -user_mailer.new_device_sign_in_before_2fa.info_p1_html.zero: Su correo electrónico y su contraseña de %{app_name} se usaron para ingresar desde un nuevo dispositivo, pero la autenticación dio error. +user_mailer.new_device_sign_in_before_2fa.info_p1_html.zero: Su correo electrónico y su contraseña de %{app_name} se usaron para ingresar desde un nuevo dispositivo, pero no se logró la autenticación. user_mailer.new_device_sign_in_before_2fa.info_p2: Si reconoce esta actividad, no tiene que hacer nada. -user_mailer.new_device_sign_in_before_2fa.info_p3_html: La autenticación de dos factores protege su cuenta de accesos no autorizados. Si no fue usted, %{reset_password_link_html} inmediatamente. -user_mailer.new_device_sign_in_before_2fa.reset_password: restablezca la contraseña +user_mailer.new_device_sign_in_before_2fa.info_p3_html: La autenticación de dos factores protege su cuenta contra un acceso no autorizado. Si no fue usted, %{reset_password_link_html}. +user_mailer.new_device_sign_in_before_2fa.reset_password: restablezca de inmediato su contraseña. user_mailer.new_device_sign_in_before_2fa.subject: Nuevo inicio de sesión con su cuenta de %{app_name} user_mailer.new_device_sign_in.disavowal_link: restablezca su contraseña user_mailer.new_device_sign_in.help_html: Si usted no hizo este cambio, %{disavowal_link_html}. Para obtener más ayuda, visite %{app_name_html} %{help_link_html} o %{contact_link_html}. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e616383b8b2..090edcf2e23 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -552,7 +552,7 @@ doc_auth.headings.document_capture_subheader_id: Permis de conduire ou carte d doc_auth.headings.document_capture_subheader_selfie: Photo de votre visage doc_auth.headings.document_capture_with_selfie: Ajouter des photos de votre pièce d’identité et une photo de vous-même doc_auth.headings.front: Recto de votre permis de conduire ou de votre carte d’identité d’un État -doc_auth.headings.how_to_verify: Choisissez la manière dont vous souhaitez confirmer votre identité +doc_auth.headings.how_to_verify: Choisir la manière dont vous souhaitez confirmer votre identité doc_auth.headings.hybrid_handoff: Comment voulez-vous ajouter votre pièce d’identité ? doc_auth.headings.hybrid_handoff_selfie: Saisir votre numéro de téléphone pour changer d’appareil doc_auth.headings.interstitial: Nous traitons vos images @@ -566,7 +566,7 @@ doc_auth.headings.ssn_update: Mettre à jour votre numéro de sécurité sociale doc_auth.headings.text_message: Nous avons envoyé un message à votre téléphone doc_auth.headings.upload_from_computer: Continuer sur cet ordinateur doc_auth.headings.upload_from_phone: Utiliser votre téléphone pour prendre des photos -doc_auth.headings.verify_at_post_office: Confirmer votre identité dans un bureau de poste +doc_auth.headings.verify_at_post_office: Confirmer votre identité en personne dans un bureau de poste doc_auth.headings.verify_identity: Confirmer votre identité doc_auth.headings.verify_online: Confirmer votre identité en ligne doc_auth.headings.verify_online_selfie: Confirmer votre identité en ligne à l’aide de votre téléphone @@ -587,9 +587,9 @@ doc_auth.info.exit.with_sp: Quitter %{app_name} et revenir à %{sp_name} doc_auth.info.exit.without_sp: Quitter la vérification d’identité et accéder à la page de votre compte doc_auth.info.getting_started_html: '%{sp_name} doit s’assurer qu’il s’agit bien de vous et non de quelqu’un qui se fait passer pour vous. %{link_html}' doc_auth.info.getting_started_learn_more: En savoir plus sur la vérification de votre identité -doc_auth.info.how_to_verify: Vous avez la possibilité de vérifier votre identité en ligne ou en personne dans un bureau de poste participant. +doc_auth.info.how_to_verify: Vous pouvez confirmer votre identité en ligne ou en personne dans un bureau de poste participant. doc_auth.info.how_to_verify_selfie: Vous avez la possibilité de confirmer votre identité en ligne avec votre téléphone ou en personne dans un bureau de poste participant. -doc_auth.info.how_to_verify_troubleshooting_options_header: Vous voulez en savoir plus sur la façon de confirmer votre identité ? +doc_auth.info.how_to_verify_troubleshooting_options_header: Vous voulez en savoir plus sur la façon de confirmer votre identité? doc_auth.info.hybrid_handoff: Nous recueillerons des informations vous concernant en lisant votre pièce d’identité délivrée par un État. doc_auth.info.hybrid_handoff_ipp_html: 'Vous n’avez pas de téléphone portable? Vous pouvez confirmer votre identité dans un bureau de poste américain participant.' doc_auth.info.image_loaded: Image chargée @@ -605,7 +605,7 @@ doc_auth.info.link_sent_complete_no_polling: Quand vous aurez fini, cliquez sur doc_auth.info.link_sent_complete_polling: L’étape suivante se chargera automatiquement. doc_auth.info.no_ssn: Vous devez avoir un numéro de sécurité sociale pour terminer la vérification de votre identité. doc_auth.info.review_examples_of_photos: Voir des exemples qui montrent comment prendre des photos nettes de votre pièce d’identité. -doc_auth.info.secure_account: Nous chiffrerons votre compte lorsque vous saisirez à nouveau votre mot de passe. Le chiffrage signifie que vos données sont protégées et que vous êtes le/la seul(e) à pouvoir accéder à vos informations ou les modifier. +doc_auth.info.secure_account: Nous chiffrons votre compte lorsque vous resaisissez votre mot de passe. Le chiffrement signifie que vos données sont protégées et que vous êtes le seul à pouvoir accéder à vos informations ou les modifier. doc_auth.info.selfie_capture_content: Nous vérifierons que vous êtes la personne figurant sur la pièce d’identité. doc_auth.info.selfie_capture_status.face_close_to_border: Trop près du cadre doc_auth.info.selfie_capture_status.face_not_found: Visage non trouvé @@ -616,15 +616,15 @@ doc_auth.info.stepping_up_html: Vérifiez à nouveau votre identité pour accéd doc_auth.info.tag: Recommandation doc_auth.info.upload_from_computer: Vous n’avez pas de téléphone ? Téléchargez les photos de votre pièce d’identité depuis cet ordinateur. doc_auth.info.upload_from_phone: Vous n’aurez pas à vous reconnecter. Vous reviendrez sur cet ordinateur après avoir pris des photos. Votre téléphone portable doit être équipé d’un appareil photo et d’un navigateur web. -doc_auth.info.verify_at_post_office_description: Cette option est préférable si vous n’avez pas de téléphone pour prendre des photos de votre pièce d’identité. +doc_auth.info.verify_at_post_office_description: Cette option est préférable si vous n’avez pas de téléphone permettant de prendre des photos de votre pièce d’identité. doc_auth.info.verify_at_post_office_description_selfie: Choisissez cette option si vous ne disposez pas d’un téléphone permettant de prendre des photos. doc_auth.info.verify_at_post_office_instruction: Vous saisirez vos données d’identification en ligne et confirmerez votre identité en personne dans un bureau de poste participant. doc_auth.info.verify_at_post_office_instruction_selfie: Vous saisirez vos données d’identification en ligne et confirmez votre identité en personne dans un bureau de poste participant. doc_auth.info.verify_at_post_office_link_text: En savoir plus sur la vérification en personne doc_auth.info.verify_identity: Nous vous demanderons votre pièce d’identité, numéro de téléphone et d’autres renseignements personnels afin de confirmer votre identité par rapport aux registres publics. -doc_auth.info.verify_online_description: Cette option est préférable si vous disposez d’un téléphone pour prendre des photos de votre pièce d’identité. +doc_auth.info.verify_online_description: Cette option est préférable si vous disposez d’un téléphone permettant de prendre des photos de votre pièce d’identité. doc_auth.info.verify_online_description_selfie: Choisissez cette option si vous disposez d’un téléphone permettant de prendre des photos. -doc_auth.info.verify_online_instruction: Vous prendrez des photos de votre pièce d’identité pour confirmer votre identité entièrement en ligne. La plupart des utilisateurs effectuent l’ensemble du processus en une seule fois. +doc_auth.info.verify_online_instruction: Vous prendrez des photos de votre pièce d’identité pour confirmer votre identité entièrement en ligne. La plupart des utilisateurs y parviennent en une seule prise. doc_auth.info.verify_online_instruction_selfie: Vous prendrez des photos de votre pièce d’identité et une photo de vous-même avec un téléphone. La plupart des utilisateurs y parviennent en une seule prise. doc_auth.info.verify_online_link_text: En savoir plus sur la vérification en ligne doc_auth.info.you_entered: 'Vous avez saisi :' @@ -1580,6 +1580,10 @@ two_factor_authentication.backup_code_prompt: Vous pouvez utiliser ce code de sa two_factor_authentication.backup_codes.instructions: Si vous n’avez pas accès à un autre appareil, conservez vos codes de sauvegarde en lieu sûr. Si vous perdez vos codes de sauvegarde, vous ne pourrez plus vous connecter à %{app_name}. two_factor_authentication.backup_codes.warning_html: 'Vous n’avez configuré que des codes de sauvegarde sur votre compte. Si vous avez accès à un autre appareil, tel qu’un téléphone, protégez votre compte à l’aide d’une autre méthode d’authentification.' two_factor_authentication.choose_another_option: '‹ Choisir une autre méthode d’authentification' +two_factor_authentication.confirm_backup_code_setup_content_html: + - Les codes de sauvegarde sont la méthode d’authentification la moins recommandée, car les codes peuvent facilement se perdre. Essayez une option plus sûre, comme une application d’authentification ou une clé de sécurité. + - Nous vous donnerons dix codes que vous pourrez télécharger, imprimer, copier ou noter. Vous saisirez un code chaque fois que vous vous connecterez. +two_factor_authentication.confirm_backup_code_setup_title: 'Êtes-vous sûr de vouloir utiliser des codes de sauvegarde ?' two_factor_authentication.form_legend: Choisissez vos méthodes d’authentification two_factor_authentication.header_text: Saisissez votre code à usage unique two_factor_authentication.important_alert_icon: icône d’alerte importante @@ -1810,7 +1814,7 @@ user_mailer.letter_reminder_14_days.subject: Terminer la vérification de votre user_mailer.letter_reminder.info_html: La lettre que vous êtes sur le point de recevoir contiendra un code de vérification nous permettant de vérifier votre adresse. Vous pouvez terminer le processus de vérification d’identité en vous connectant à %{link_html} et en saisissant le code de vérification. user_mailer.letter_reminder.subject: Nous avons posté une lettre à l’adresse que vous avez enregistrée user_mailer.new_device_sign_in_after_2fa.authentication_methods: méthodes d’authentification -user_mailer.new_device_sign_in_after_2fa.info_p1: Votre adresse e-mail et votre mot de passe %{app_name} ont été utilisés pour se connecter et s’authentifier sur un nouvel appareil. +user_mailer.new_device_sign_in_after_2fa.info_p1: Votre e-mail et mot de passe %{app_name} ont été utilisés pour se connecter et s’authentifier sur un nouvel appareil. user_mailer.new_device_sign_in_after_2fa.info_p2: Si vous reconnaissez cette activité, vous n’avez rien à faire. user_mailer.new_device_sign_in_after_2fa.info_p3_html: Si ce n’est pas vous, %{reset_password_link_html} et modifiez immédiatement vos %{authentication_methods_link_html}. user_mailer.new_device_sign_in_after_2fa.reset_password: réinitialisez votre mot de passe @@ -1818,13 +1822,13 @@ user_mailer.new_device_sign_in_after_2fa.subject: Nouvelle connexion et authenti user_mailer.new_device_sign_in_attempts.events.sign_in_after_2fa: Signé avec deuxième facteur user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa: Connecté avec mot de passe user_mailer.new_device_sign_in_attempts.events.sign_in_unsuccessful_2fa: Échec de l’authentification -user_mailer.new_device_sign_in_attempts.new_sign_in_from: Nouvelle connexion potentiellement située à %{location} -user_mailer.new_device_sign_in_before_2fa.info_p1_html.one: Votre adresse électronique et votre mot de passe %{app_name} ont été utilisés pour vous connecter à partir d’un nouvel appareil, mais l’authentification a échoué. -user_mailer.new_device_sign_in_before_2fa.info_p1_html.other: Votre adresse électronique et votre mot de passe %{app_name} ont été utilisés pour vous connecter à partir d’un nouvel appareil, mais l’authentification a échoué %{count} reprises. -user_mailer.new_device_sign_in_before_2fa.info_p1_html.zero: Votre adresse électronique et votre mot de passe %{app_name} ont été utilisés pour vous connecter à partir d’un nouvel appareil, mais l’authentification a échoué. +user_mailer.new_device_sign_in_attempts.new_sign_in_from: Nouvelle connexion potentiellement localisée à %{location} +user_mailer.new_device_sign_in_before_2fa.info_p1_html.one: Votre e-mail et mot de passe %{app_name} ont été utilisés pour vous connecter à partir d’un nouvel appareil, mais l’authentification a échoué. +user_mailer.new_device_sign_in_before_2fa.info_p1_html.other: Votre e-mail et mot de passe %{app_name} ont été utilisés pour vous connecter à partir d’un nouvel appareil, mais l’authentification a échoué %{count} reprises. +user_mailer.new_device_sign_in_before_2fa.info_p1_html.zero: Votre e-mail et mot de passe %{app_name} ont été utilisés pour vous connecter à partir d’un nouvel appareil, mais l’authentification a échoué. user_mailer.new_device_sign_in_before_2fa.info_p2: Si vous reconnaissez cette activité, vous n’avez rien à faire. -user_mailer.new_device_sign_in_before_2fa.info_p3_html: L’authentification à deux facteurs protège votre compte contre tout accès non autorisé. Si ce n’est pas vous, %{reset_password_link_html}. -user_mailer.new_device_sign_in_before_2fa.reset_password: réinitialisez immédiatement votre mot de passe +user_mailer.new_device_sign_in_before_2fa.info_p3_html: L’authentification à deux facteurs protège votre compte des accès non autorisés. Si vous n’êtes pas à l’origine de cette action, %{reset_password_link_html}. +user_mailer.new_device_sign_in_before_2fa.reset_password: veuillez réinitialiser immédiatement votre mot de passe. user_mailer.new_device_sign_in_before_2fa.subject: Nouvelle connexion avec votre compte %{app_name} user_mailer.new_device_sign_in.disavowal_link: réinitialisez votre mot de passe user_mailer.new_device_sign_in.help_html: Si vous n’avez pas effectué ce changement, %{disavowal_link_html}. Pour plus d’aide, veuillez visiter le %{help_link_html} de %{app_name_html} ou %{contact_link_html}. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 979f994230b..853c7cdc500 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -494,8 +494,8 @@ doc_auth.errors.alerts.id_not_recognized: 我们无法辨认你的身份证件 doc_auth.errors.alerts.id_not_verified: 我们无法验证你的身份证件。可能因为你拍照时身份证件移动了。尝试重拍。 doc_auth.errors.alerts.issue_date_checks: 我们读取不到你身份证件上的颁发日期。尝试重拍。 doc_auth.errors.alerts.ref_control_number_check: 我们读取不到控制号码条码。尝试重拍。 -doc_auth.errors.alerts.selfie_not_live_help_link_text: Review more tips for taking a clear photo of yourself -doc_auth.errors.alerts.selfie_not_live_or_poor_quality: Try taking a photo of yourself again. Make sure your whole face is clear and visible in the photo. +doc_auth.errors.alerts.selfie_not_live_help_link_text: 查看更多有关清晰拍摄你本人照片的提示 +doc_auth.errors.alerts.selfie_not_live_or_poor_quality: 尝试再拍一张你自己。确保照片中你整个面孔都清楚可见。 doc_auth.errors.alerts.sex_check: 我们读取不到你身份证件上的性别。尝试重拍。 doc_auth.errors.alerts.visible_color_check: 我们无法验证你的身份证件。可能你拍照时身份证件移动了,或者照片太暗。尝试在亮一点的灯光下重拍。 doc_auth.errors.alerts.visible_photo_check: 我们无法验证你身份证件上的照片。尝试重拍。 @@ -518,7 +518,7 @@ doc_auth.errors.general.multiple_front_id_failures: 我们无法验证你身份 doc_auth.errors.general.network_error: 我们这边有技术困难。请稍后再提交你的图像。 doc_auth.errors.general.no_liveness: 尝试重拍。 doc_auth.errors.general.selfie_failure: 我们无法验证你自己的照片。请重拍一张。 -doc_auth.errors.general.selfie_failure_help_link_text: Review more tips for taking clear photos +doc_auth.errors.general.selfie_failure_help_link_text: 查看更多有关拍摄清晰照片的提示 doc_auth.errors.glare.failed_short: 图像有炫光,请再试一次。 doc_auth.errors.glare.top_msg: 我们无法读取你的身份证件。你的照片可能有炫光。确保你相机上的闪光是关掉的,然后再尝试重拍张。 doc_auth.errors.glare.top_msg_plural: 我们无法读取你的身份证件。你的照片可能有炫光。确保你相机上的闪光是关掉的,然后再尝试重拍。 @@ -547,14 +547,14 @@ doc_auth.headings.capture_scan_warning_link: 上传新照片 doc_auth.headings.document_capture: 添加你身份证件的照片 doc_auth.headings.document_capture_back: 你身份证件的背面 doc_auth.headings.document_capture_front: 你身份证件的正面 -doc_auth.headings.document_capture_selfie: 你本人照片 +doc_auth.headings.document_capture_selfie: 你面部照片 doc_auth.headings.document_capture_subheader_id: 驾照或州政府颁发的身份证件 doc_auth.headings.document_capture_subheader_selfie: 你本人照片 doc_auth.headings.document_capture_with_selfie: 添加你身份证件和本人的照片 doc_auth.headings.front: 驾照或州政府颁发身份证件的正面 doc_auth.headings.how_to_verify: 选择你想如何验证身份 doc_auth.headings.hybrid_handoff: 你想怎么添加身份证件? -doc_auth.headings.hybrid_handoff_selfie: Enter your phone number to switch devices +doc_auth.headings.hybrid_handoff_selfie: 输入电话号码来切换设备 doc_auth.headings.interstitial: 我们正在处理你的图像 doc_auth.headings.lets_go: 你的身份是如何验证的 doc_auth.headings.no_ssn: 没有社会保障号码? @@ -568,7 +568,7 @@ doc_auth.headings.upload_from_computer: 在这台电脑上继续 doc_auth.headings.upload_from_phone: 使用手机拍照 doc_auth.headings.verify_at_post_office: 去邮局验证身份 doc_auth.headings.verify_identity: 验证你的身份 -doc_auth.headings.verify_online: 在网上验证你的身份 +doc_auth.headings.verify_online: 在网上验证身份 doc_auth.headings.verify_online_selfie: 使用手机在网上验证你的身份 doc_auth.headings.welcome: 开始验证你的身份 doc_auth.hybrid_flow_warning.explanation_html: 你在使用 %{app_name} 验证身份以访问 %{service_provider_name} 及其服务。 @@ -587,11 +587,11 @@ doc_auth.info.exit.with_sp: 退出 %{app_name} 并返回 %{app_name} doc_auth.info.exit.without_sp: 退出身份验证并到你的账户页面 doc_auth.info.getting_started_html: '%{sp_name} needs to make sure you are you — not someone pretending to be you. %{link_html}' doc_auth.info.getting_started_learn_more: Learn more about verifying your identity -doc_auth.info.how_to_verify: 你有在网上验证身份或者到参与邮件亲身验证身份的选择 +doc_auth.info.how_to_verify: 你可以选择在网上验证身份或者到参与本项目的邮局亲身验证身份。 doc_auth.info.how_to_verify_selfie: 你可以选择用你的手机在网上验证身份或者到参与本项目的邮局亲身验证身份。 doc_auth.info.how_to_verify_troubleshooting_options_header: 想对验证身份获得更多了解吗? doc_auth.info.hybrid_handoff: 我们将通过读取州政府颁发给你的身份证件来收集你的信息。 -doc_auth.info.hybrid_handoff_ipp_html: 'Don’t have a mobile phone? You can verify your identity at a United States Post Office instead.' +doc_auth.info.hybrid_handoff_ipp_html: '没有手机?你可以去一个美国邮局验证身份。' doc_auth.info.image_loaded: 图像已加载 doc_auth.info.image_loading: 图像加载中 doc_auth.info.image_updated: 图像已上传 @@ -606,25 +606,25 @@ doc_auth.info.link_sent_complete_polling: 下一步会自动加载。 doc_auth.info.no_ssn: 你必须有社会保障号码才能完成身份验证。 doc_auth.info.review_examples_of_photos: 查看如何拍出身份证件清晰照片的示例。 doc_auth.info.secure_account: 我们会用你的密码对你的账户加密。加密意味着你的数据得到了保护,而且只有你能够访问或变更你的信息。 -doc_auth.info.selfie_capture_content: We’ll check that you are the person on your ID. -doc_auth.info.selfie_capture_status.face_close_to_border: Too close to the frame -doc_auth.info.selfie_capture_status.face_not_found: Face not found -doc_auth.info.selfie_capture_status.face_too_small: Face too small -doc_auth.info.selfie_capture_status.too_many_faces: Too many faces +doc_auth.info.selfie_capture_content: 我们会查看你是身份证件上的人。 +doc_auth.info.selfie_capture_status.face_close_to_border: 距离相框太近 +doc_auth.info.selfie_capture_status.face_not_found: 没找到面孔 +doc_auth.info.selfie_capture_status.face_too_small: 面孔太小 +doc_auth.info.selfie_capture_status.too_many_faces: 太多张面孔 doc_auth.info.ssn: 我们需要你的社会保障号码来证实你的姓名、生日和地址。 doc_auth.info.stepping_up_html: Verify your identity again to access this service. %{link_html} doc_auth.info.tag: 建议 doc_auth.info.upload_from_computer: 没有手机?从该电脑上传你身份证件的照片。 doc_auth.info.upload_from_phone: 你不必重新登录,而且拍完照后可以转回到该电脑。你的手机必须带有相机和网络浏览器。 -doc_auth.info.verify_at_post_office_description: 如果你没有移动设备或者无法轻松拍身份证件照片,这一选项更好。 +doc_auth.info.verify_at_post_office_description: 如果你没有手机拍身份证件照片,这一选项更好。 doc_auth.info.verify_at_post_office_description_selfie: 如果你没有手机可以拍照,请选择该选项 -doc_auth.info.verify_at_post_office_instruction: 你在网上输入身份证件信息,然后到一个参与邮件去亲身验证身份。 +doc_auth.info.verify_at_post_office_instruction: 你在网上输入身份证件信息,然后到一个参与本项目的邮局去亲身验证身份。 doc_auth.info.verify_at_post_office_instruction_selfie: 你在网上输入身份证件信息,然后到一个参与本项目的邮局去亲身验证身份。 doc_auth.info.verify_at_post_office_link_text: 对亲身验证获得更多了解 doc_auth.info.verify_identity: 我们会要求获得你的个人信息并通过与公共记录核对来验证你的身份。 doc_auth.info.verify_online_description: 如果你没有移动设备或者无法轻松拍身份证件照片,这一选项更好。 doc_auth.info.verify_online_description_selfie: 如果你有手机可以拍照,请选择该选项。 -doc_auth.info.verify_online_instruction: 你将拍自己身份证件的照片来完全在网上验证身份。大多数用户都能轻松完成这一流程。 +doc_auth.info.verify_online_instruction: 你将拍身份证件的照片来完全在网上验证身份。大多数用户都能轻松完成这样流程。 doc_auth.info.verify_online_instruction_selfie: 使用手机在网上验证你的身份你将用手机拍摄身份证件和本人的照片。大多数用户都能轻松完成这一流程。 doc_auth.info.verify_online_link_text: 对网上验证获得更多了解 doc_auth.info.you_entered: 你输入了: @@ -643,16 +643,16 @@ doc_auth.instructions.text2: 你不需要有卡。 doc_auth.instructions.text3: We match your phone number with your personal information and send a one-time code to your phone. doc_auth.instructions.text4: Your password saves and encrypts your personal information. doc_auth.tips.document_capture_hint: 必须是 JPG 或 PNG -doc_auth.tips.document_capture_id_text1: 使用暗色背景 -doc_auth.tips.document_capture_id_text2: 在平坦平面上拍照。 -doc_auth.tips.document_capture_id_text3: 不要使用相机的闪光灯 +doc_auth.tips.document_capture_id_text1: 把你身份证件放在一个平坦、暗色的表面上。 +doc_auth.tips.document_capture_id_text2: 在光线明亮的地方拍照。 +doc_auth.tips.document_capture_id_text3: 避免身份证件上有炫光或阴影。 doc_auth.tips.document_capture_id_text4: 文件大小应当至少 2 兆。 -doc_auth.tips.document_capture_selfie_id_header_text: 拍出清晰照片的提示 -doc_auth.tips.document_capture_selfie_selfie_text: 拍出清晰照片的提示 -doc_auth.tips.document_capture_selfie_text1: 把设备放在眼睛水平 -doc_auth.tips.document_capture_selfie_text2: 确保你整个面孔都可以看到 -doc_auth.tips.document_capture_selfie_text3: 在光线充沛的地方拍照 -doc_auth.tips.document_capture_selfie_text4: Make sure your whole face is visible within the green circle. +doc_auth.tips.document_capture_selfie_id_header_text: 如何拍摄你身份证件的清晰照片 +doc_auth.tips.document_capture_selfie_selfie_text: 如何准备拍照 +doc_auth.tips.document_capture_selfie_text1: 摘掉可能盖住你面部的任何服饰。我们建议你摘掉眼镜和帽子。 +doc_auth.tips.document_capture_selfie_text2: 在光线明亮的地方拍照。 +doc_auth.tips.document_capture_selfie_text3: 面部表情保持中性。 +doc_auth.tips.document_capture_selfie_text4: 确保你整个面部都可以在绿色圆圈中看到。 doc_auth.tips.mobile_phone_required: 要求有手机 doc_auth.tips.review_issues_id_header_text: 请仔细查看你州政府颁发身份证件的图像: doc_auth.tips.review_issues_id_text1: 你是否使用了暗色背景? @@ -686,8 +686,8 @@ errors.doc_auth.phone_step_incomplete: 在继续之前你必须使用手机上 errors.doc_auth.rate_limited_heading: 我们无法验证你的身份证件。 errors.doc_auth.rate_limited_subheading: 尝试再拍照片 errors.doc_auth.rate_limited_text_html: 为了你的安全,我们限制你在网上尝试验证文件的次数。 %{timeout} 后再试。 -errors.doc_auth.selfie_fail_heading: We couldn’t match the photo of yourself to your ID -errors.doc_auth.selfie_not_live_or_poor_quality_heading: We could not verify the photo of yourself +errors.doc_auth.selfie_fail_heading: 我们无法把你自己的照片与你的身份证件匹配 +errors.doc_auth.selfie_not_live_or_poor_quality_heading: 我们无法验证你自己的照片 errors.doc_auth.send_link_limited: 你尝试了太多次。请在 %{timeout}后再试。你也可以返回并选择使用电脑。 errors.enter_code.rate_limited_html: 你输入错误验证码太多次。 %{timeout} 后再试。 errors.general: 哎呀,出错了。请再试一次。 @@ -809,8 +809,8 @@ forms.buttons.back: 返回 forms.buttons.cancel: 是的,取消 forms.buttons.confirm: 确认 forms.buttons.continue: 继续 -forms.buttons.continue_ipp: Continue in person -forms.buttons.continue_remote: Continue online +forms.buttons.continue_ipp: 继续亲身验证 +forms.buttons.continue_remote: 继续在网上验证 forms.buttons.continue_remote_selfie: 在手机上继续 forms.buttons.delete: 删除 forms.buttons.disable: 删除 @@ -1584,6 +1584,10 @@ two_factor_authentication.backup_code_prompt: 该安全代码可使用一次。 two_factor_authentication.backup_codes.instructions: 如果你无法使用另一个设备,请将安全代码存放好。万一丢失备用代码,你就无法登录%{app_name}。 two_factor_authentication.backup_codes.warning_html: '你只在自己账户上设置了备用代码。如果你可以使用另一个设备,比如手机,请用另一种身份证实方法来保护你的账户。' two_factor_authentication.choose_another_option: '‹ 选择另一个身份证实方法' +two_factor_authentication.confirm_backup_code_setup_content_html: + - '备用代码是最不宜使用的身份证实方法,因为备用代码很容易丢失。尝试一个更安全的选项,比如身份证实应用程序或安全密钥。' + - 我们会给你 10 个代码供你下载、打印、复制或记下。每次你登录时输入一个代码。 +two_factor_authentication.confirm_backup_code_setup_title: 你确定要使用备用代码吗? two_factor_authentication.form_legend: 选择你的身份证实方法 two_factor_authentication.header_text: 输入一次性代码 two_factor_authentication.important_alert_icon: 重要警告标志 @@ -1815,22 +1819,22 @@ user_mailer.letter_reminder_14_days.subject: 完成验证你的身份 user_mailer.letter_reminder.info_html: 你将收到的信件会含有帮助我们验证你地址的一次性代码。你可以登入 %{link_html} 并输入该一次性代码来完成身份验证流i程。 user_mailer.letter_reminder.subject: 我们已向你存档地址发送了一封信。 user_mailer.new_device_sign_in_after_2fa.authentication_methods: authentication methods -user_mailer.new_device_sign_in_after_2fa.info_p1: Your %{app_name} email and password were used to sign-in and authenticate on a new device. -user_mailer.new_device_sign_in_after_2fa.info_p2: If you recognize this activity, you don’t need to do anything. +user_mailer.new_device_sign_in_after_2fa.info_p1: 你的 %{app_name} 电邮和密码在一个新设备上被用来登录和进行身份验证。 +user_mailer.new_device_sign_in_after_2fa.info_p2: 如果你知道该活动,则无需做任何事情。 user_mailer.new_device_sign_in_after_2fa.info_p3_html: If this wasn’t you, %{reset_password_link_html} and change your %{authentication_methods_link_html} immediately. user_mailer.new_device_sign_in_after_2fa.reset_password: reset your password -user_mailer.new_device_sign_in_after_2fa.subject: New sign-in and authentication with your %{app_name} account +user_mailer.new_device_sign_in_after_2fa.subject: '%{app_name} 账户有新的登录和身份验证' user_mailer.new_device_sign_in_attempts.events.sign_in_after_2fa: Authenticated user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa: Signed in with password user_mailer.new_device_sign_in_attempts.events.sign_in_unsuccessful_2fa: Failed to authenticate -user_mailer.new_device_sign_in_attempts.new_sign_in_from: New sign-in potentially located in %{location} +user_mailer.new_device_sign_in_attempts.new_sign_in_from: 新登录可能是在 %{location} user_mailer.new_device_sign_in_before_2fa.info_p1_html.one: Your %{app_name} email and password were used to sign in from a new device but failed to authenticate. user_mailer.new_device_sign_in_before_2fa.info_p1_html.other: Your %{app_name} email and password were used to sign in from a new device but failed to authenticate %{count} times. user_mailer.new_device_sign_in_before_2fa.info_p1_html.zero: Your %{app_name} email and password were used to sign in from a new device but failed to authenticate. -user_mailer.new_device_sign_in_before_2fa.info_p2: If you recognize this activity, you don’t need to do anything. -user_mailer.new_device_sign_in_before_2fa.info_p3_html: Two-factor authentication protects your account from unauthorized access. If this wasn’t you, %{reset_password_link_html} immediately. -user_mailer.new_device_sign_in_before_2fa.reset_password: reset your password -user_mailer.new_device_sign_in_before_2fa.subject: New sign-in with your %{app_name} account +user_mailer.new_device_sign_in_before_2fa.info_p2: 如果你知道该活动,则无需做任何事情。 +user_mailer.new_device_sign_in_before_2fa.info_p3_html: 双重身份验证保护你账户不受未经授权的访问。如果不是你,请马上 %{reset_password_link_html}. +user_mailer.new_device_sign_in_before_2fa.reset_password: 重设密码 +user_mailer.new_device_sign_in_before_2fa.subject: 你 %{app_name} 账户有新的登录 user_mailer.new_device_sign_in.disavowal_link: 重设你的密码 user_mailer.new_device_sign_in.help_html: 如果你没做此更改, %{disavowal_link_html}。要得到更多帮助,请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。 user_mailer.new_device_sign_in.info: '' diff --git a/config/routes.rb b/config/routes.rb index 356d928a985..6680e102702 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -285,6 +285,8 @@ as: :user_two_factor_authentication # route name is used by two_factor_authentication gem get '/backup_code_refreshed' => 'users/backup_code_setup#refreshed' get '/backup_code_reminder' => 'users/backup_code_setup#reminder' + get '/backup_code_confirm_setup' => 'users/backup_code_setup#new' + post '/backup_code_setup' => 'users/backup_code_setup#create' get '/backup_code_setup' => 'users/backup_code_setup#index' patch '/backup_code_continue' => 'users/backup_code_setup#continue' get '/backup_code_regenerate' => 'users/backup_code_setup#edit' diff --git a/docs/frontend.md b/docs/frontend.md index 6dd47a3eb65..0f101a91447 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -376,9 +376,9 @@ Note that NewRelic creates links in stack traces which are invalid, since they i Debugging these stack traces can be difficult, since files in production are minified, and the stack traces include line numbers and columns for minified files. With the following steps, you can find a reference to the original code: 1. Download the minified JavaScript file referenced in the stack trace - - Example: https://secure.login.gov/packs/js/document-capture-e41c853e.digested.js + - Example: https://secure.login.gov/packs/document-capture-e41c853e.digested.js 2. Download the sourcemap file for the JavaScript by appending `.map` to the previous URL - - Example: https://secure.login.gov/packs/js/document-capture-e41c853e.digested.js.map + - Example: https://secure.login.gov/packs/document-capture-e41c853e.digested.js.map 3. Install the [`sourcemap-lookup` npm package](https://www.npmjs.com/package/sourcemap-lookup) - `npm i -g sourcemap-lookup` 4. Open a terminal window to the directory where you downloaded the files in steps 1 and 2 @@ -390,6 +390,24 @@ Debugging these stack traces can be difficult, since files in production are min The output of the `sourcemap-lookup` command should include "Original Position" and "Code Section" of the code which triggered the error. +## Fonts + +Font files are optimized to remove unused character data. If a new character is added to content, the font files must be regenerated: + +1. [Download Public Sans](https://public-sans.digital.gov/) and extract it to your project's `tmp/` directory +2. Install [glyphhanger](https://github.com/zachleat/glyphhanger) and its dependencies: + 1. `npm install -g glyphhanger` + 2. `pip install fonttools brotli` +3. Scrape content for character data: + 1. `make lint_font_glyphs` +4. Subset the original Public Sans fonts to include only used character data: + 1. `glyphhanger app/assets/fonts/glyphs.txt --formats=woff2 --subset="tmp/public-sans-v2/fonts/ttf/PublicSans-*.ttf"` +5. Replace font files with new subset fonts: + 1. `cd tmp/public-sans-v2/fonts/ttf` + 2. `find . -name "*-subset.woff2" -exec sh -c 'cp $1 "../../../../app/assets/fonts/public-sans/${1%-subset.woff2}.woff2"' _ {} \;` + +At this point, your working directory should reflect changes to all of the files within `app/assets/fonts/public-sans`. + ## Devices The application should support: diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 87e472b033b..f2f2462fde3 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -74,6 +74,7 @@ def self.store config.add(:aws_kms_session_key_id, type: :string) config.add(:aws_logo_bucket, type: :string) config.add(:aws_region, type: :string) + config.add(:backup_code_confirm_setup_screen_enabled, type: :boolean) config.add(:backup_code_cost, type: :string) config.add(:broken_personal_key_window_finish, type: :timestamp) config.add(:broken_personal_key_window_start, type: :timestamp) diff --git a/scripts/merge_yml b/scripts/merge_yml deleted file mode 100755 index f85b037b4fd..00000000000 --- a/scripts/merge_yml +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Merges YML files, in order of least to greatest precedence -# Intended to only be used for flat yml file migration for translations -require 'yaml' - -first, *rest = ARGV - -combined = rest.reduce(YAML.load_file(first) || {}) do |accumulator, path| - accumulator.merge(YAML.load_file(path) || {}) -end - -puts combined.to_yaml diff --git a/scripts/yaml_characters b/scripts/yaml_characters new file mode 100755 index 00000000000..9be80fe72b1 --- /dev/null +++ b/scripts/yaml_characters @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'pathname' +require_relative '../config/environment' + +excluded_locales = [] +excluded_gem_paths = [] +OptionParser.new do |opts| + opts.banner = <<~TXT + Usage + ======================= + + #{$PROGRAM_NAME} [OPTIONS] + + Prints all unique characters from Rails loaded I18n data, which can be used in combination + with font subsetting to optimize font sizes. + + TXT + + opts.on('--exclude-locale=LOCALE', 'Disregard characters from the given locale') do |locale| + excluded_locales << locale.to_sym + end + + opts.on('--exclude-gem-path=GEM', 'Disregard characters loaded by the given gem') do |gem| + excluded_gem_paths << Gem.loaded_specs[gem].full_gem_path + end + + opts.on('-h', '--help', 'Prints this help') do + STDOUT.puts opts + exit + end +end.parse!(ARGV) + +def sanitize(string) + string.gsub(/<.+?>/, '').gsub(/%{.+?}/, '').gsub(/[\n\r\t]/, '') +end + +def hash_values(hash) + hash.values.flat_map do |value| + case value + when Hash + hash_values(value) + when Array + value + else + [value] + end + end +end + +I18n.load_path.reject! do |load_path| + excluded_gem_paths.any? { |gem_path| load_path.start_with?(gem_path) } +end + +I18n.backend.eager_load! + +data = I18n.backend.translations.slice(*I18n.available_locales - excluded_locales) +strings = hash_values(data) +joined_string = strings.join('') +sanitized_string = sanitize(joined_string) +characters = sanitized_string.chars.to_a.uniq.sort.join('') + +STDOUT.puts characters diff --git a/scripts/yml_fix_merge_conflicts b/scripts/yml_fix_merge_conflicts deleted file mode 100755 index 618555052cd..00000000000 --- a/scripts/yml_fix_merge_conflicts +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -xeuo pipefail - -# Script to help fix merge conflicts when migrating to the flat yml style -# Run it after doing a `git merge origin/main` into a feature branch - -# Intended to be a temporary script only for flat yml migration - -# git checkout origin/main -- config/locales/ - -locales="en es fr zh" - -function find_locale_files() { - locale=$1 - - find config/locales -type f -name "*${locale}.yml" | \ - grep -v telephony | \ - grep -v transliterate -} - -for locale in $locales; do - find_locale_files "$locale" | - xargs ./scripts/yml_to_flat_yml > "tmp/newer_${locale}.yml" - - if [ -f "config/locales/${locale}.yml" ]; then - cp "config/locales/${locale}.yml" "tmp/older_${locale}.yml" - else - echo > "tmp/older_${locale}.yml" - fi - - ./scripts/merge_yml \ - "tmp/older_${locale}.yml" \ - "tmp/newer_${locale}.yml" \ - > "config/locales/${locale}.yml" -done - -if [[ "${1:-}" == "--force" ]]; then - git status --porcelain | grep "DU " | cut -d' ' -f 2 | xargs git rm - - for locale in $locales; do - find_locale_files "$locale" | \ - grep -v "config/locales/${locale}.yml" | \ - xargs git rm -f - done -fi - -make normalize_yaml - -for locale in $locales; do - git add "config/locales/${locale}.yml" -done diff --git a/scripts/yml_to_flat_yml b/scripts/yml_to_flat_yml deleted file mode 100755 index 90443675c37..00000000000 --- a/scripts/yml_to_flat_yml +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Script to help migrating i18n files to our "flat yml" format -# Can probably be removed after migration is complete - -require 'yaml' -require 'json' - -combined = {} - -# @yieldparam keypath [Array] -# @yieldparam value -def each_full_key(obj, keypath: [], &block) - if obj.is_a?(Hash) - obj.each do |key, value| - each_full_key(value, keypath: keypath + [key], &block) - end - elsif obj.is_a?(Array) - obj.each_with_index do |item, idx| - each_full_key(item, keypath: keypath + [idx], &block) - end - else - yield keypath, obj - end -end - -if ARGV.empty? - puts <<~EOS - Usage: - - #{File.basename($PROGRAM_NAME)} FILE [OTHER...] - - Takes each YAML file provided as an argument, combines them into one hash - and "flattens" the keys - EOS - - exit 1 -end - -ARGV.each do |filename| - each_full_key(YAML.load_file(filename)) do |(_locale, *keypath), value| - combined[keypath.map(&:to_s).join('.')] = value - end -end - -combined.sort_by { |k, _v| k }.each do |flat_key, value| - STDOUT.puts "#{flat_key}: #{value.to_json}" if !flat_key.empty? -end diff --git a/spec/controllers/concerns/new_device_concern_spec.rb b/spec/controllers/concerns/new_device_concern_spec.rb new file mode 100644 index 00000000000..1cb413cdf45 --- /dev/null +++ b/spec/controllers/concerns/new_device_concern_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +RSpec.describe NewDeviceConcern, type: :controller do + let(:test_class) do + Class.new do + include NewDeviceConcern + + attr_reader :current_user, :user_session, :cookies + + def initialize(current_user:, user_session:, cookies:) + @current_user = current_user + @user_session = user_session + @cookies = cookies + end + end + end + + let(:cookies) { {} } + let(:current_user) { create(:user) } + let(:user_session) { {} } + let(:instance) { test_class.new(current_user:, user_session:, cookies:) } + + describe '#set_new_device_session' do + context 'with new device' do + it 'sets user session value to true' do + instance.set_new_device_session + + expect(user_session[:new_device]).to eq(true) + end + end + + context 'with authenticated device' do + let(:current_user) { create(:user, :with_authenticated_device) } + let(:cookies) { { device: current_user.devices.last.cookie_uuid } } + + it 'sets user session value to false' do + instance.set_new_device_session + + expect(user_session[:new_device]).to eq(false) + end + end + end + + describe '#new_device?' do + subject(:new_device?) { instance.new_device? } + + context 'session value is unassigned' do + it { expect(new_device?).to eq(true) } + end + + context 'session value is true' do + let(:user_session) { { new_device: true } } + + it { expect(new_device?).to eq(true) } + end + + context 'session value is false' do + let(:user_session) { { new_device: false } } + + it { expect(new_device?).to eq(false) } + end + end +end diff --git a/spec/controllers/concerns/remember_device_concern_spec.rb b/spec/controllers/concerns/remember_device_concern_spec.rb index c3f80e20cfe..6c6ecfc13ba 100644 --- a/spec/controllers/concerns/remember_device_concern_spec.rb +++ b/spec/controllers/concerns/remember_device_concern_spec.rb @@ -3,20 +3,22 @@ RSpec.describe RememberDeviceConcern do let(:sp) { nil } let(:raw_session) { {} } + let(:current_user) { build(:user) } subject(:test_controller) do test_controller_class = Class.new(ApplicationController) do include(RememberDeviceConcern) - attr_reader :sp, :raw_session, :request + attr_reader :sp, :raw_session, :request, :current_user alias :sp_from_sp_session :sp alias :sp_session :raw_session - def initialize(sp, raw_session, request) + def initialize(sp, raw_session, request, current_user) @sp = sp @raw_session = raw_session @request = request + @current_user = current_user end end @@ -27,7 +29,7 @@ def initialize(sp, raw_session, request) filtered_parameters: {}, ) - test_controller_class.new(sp, raw_session, test_request) + test_controller_class.new(sp, raw_session, test_request, current_user) end describe '#mfa_expiration_interval' do diff --git a/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb b/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb index 82f6d0b25f6..7ba560c4fc2 100644 --- a/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb +++ b/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb @@ -64,7 +64,7 @@ context 'with an existing device' do before do - controller.user_session[:new_device] = false + allow(controller).to receive(:new_device?).and_return(false) end it 'does not send an alert' do @@ -76,7 +76,7 @@ context 'with a new device' do before do - controller.user_session[:new_device] = true + allow(controller).to receive(:new_device?).and_return(true) end it 'sends the new device alert using 2fa event date' do @@ -119,7 +119,7 @@ context 'with an existing device' do before do - controller.user_session[:new_device] = false + allow(controller).to receive(:new_device?).and_return(false) end it 'does not send an alert' do @@ -131,7 +131,7 @@ context 'with a new device' do before do - controller.user_session[:new_device] = true + allow(controller).to receive(:new_device?).and_return(true) end it 'sends the new device alert' do diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb index 9568b4053dc..9c3cb7434d9 100644 --- a/spec/controllers/idv/agreement_controller_spec.rb +++ b/spec/controllers/idv/agreement_controller_spec.rb @@ -228,9 +228,9 @@ }.compact end - it 'redirects to idv agreement' do + it 'renders the form again' do put :update, params: params - expect(response).to redirect_to(idv_agreement_url) + expect(response).to render_template('idv/agreement/show') end end end diff --git a/spec/controllers/idv/how_to_verify_controller_spec.rb b/spec/controllers/idv/how_to_verify_controller_spec.rb index 7bbd883d2a3..3b20836174d 100644 --- a/spec/controllers/idv/how_to_verify_controller_spec.rb +++ b/spec/controllers/idv/how_to_verify_controller_spec.rb @@ -42,6 +42,7 @@ expect(Idv::HowToVerifyController.enabled?).to be false expect(subject.idv_session.skip_doc_auth).to be_nil + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be_nil expect(response).to redirect_to(idv_hybrid_handoff_url) end end @@ -57,6 +58,7 @@ expect(Idv::HowToVerifyController.enabled?).to be false expect(subject.idv_session.skip_doc_auth).to be_nil + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be_nil expect(response).to redirect_to(idv_hybrid_handoff_url) end end @@ -72,6 +74,7 @@ expect(Idv::HowToVerifyController.enabled?).to be false expect(subject.idv_session.skip_doc_auth).to be_nil + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be_nil expect(response).to redirect_to(idv_hybrid_handoff_url) end end @@ -84,6 +87,7 @@ expect(Idv::HowToVerifyController.enabled?).to be true expect(subject.idv_session.service_provider.in_person_proofing_enabled).to be true expect(subject.idv_session.skip_doc_auth).to be_nil + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be_nil expect(response).to render_template :show end end @@ -120,6 +124,7 @@ get :show expect(subject.idv_session.skip_doc_auth).to be_nil + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be_nil expect(response).to render_template :show end @@ -193,6 +198,7 @@ put :update, params: params expect(subject.idv_session.skip_doc_auth).to be false + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be false expect(response).to redirect_to(idv_hybrid_handoff_url) end @@ -220,6 +226,7 @@ put :update, params: params expect(subject.idv_session.skip_doc_auth).to be true + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be true expect(response).to redirect_to(idv_document_capture_url) end @@ -235,6 +242,7 @@ put :update, params: { undo_step: true } expect(subject.idv_session.skip_doc_auth).to be_nil + expect(subject.idv_session.skip_doc_auth_from_how_to_verify).to be_nil expect(subject.idv_session.opted_in_to_in_person_proofing).to be_nil expect(response).to redirect_to(idv_how_to_verify_url) end diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index e16b188831b..f48970971e0 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -218,6 +218,7 @@ allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode). and_return(false) subject.idv_session.skip_doc_auth = nil + subject.idv_session.skip_doc_auth_from_how_to_verify = nil end it 'redirects to how to verify' do @@ -241,6 +242,7 @@ allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode). and_return(false) subject.idv_session.skip_doc_auth = true + subject.idv_session.skip_doc_auth_from_how_to_verify = true subject.idv_session.skip_hybrid_handoff = true end @@ -255,6 +257,7 @@ before do subject.idv_session.service_provider.in_person_proofing_enabled = false subject.idv_session.skip_doc_auth = nil + subject.idv_session.skip_doc_auth_from_how_to_verify = nil end it 'renders the show template' do diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index aa036df6249..b458af87f1a 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -478,6 +478,8 @@ selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: a_kind_of(Hash), + id_issued_status: 'present', + id_expiration_status: 'present', ) expect_funnel_update_counts(user, 1) @@ -669,6 +671,8 @@ Front: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), Back: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), ), + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -780,6 +784,8 @@ Front: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), Back: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), ), + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -888,6 +894,8 @@ selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: hash_including(:Front, :Back), + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -996,6 +1004,8 @@ selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: hash_including(:Front, :Back), + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end diff --git a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb index 8c2cb22cff7..8884908d96a 100644 --- a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb @@ -3,7 +3,7 @@ RSpec.describe TwoFactorAuthentication::BackupCodeVerificationController do let(:user) { create(:user) } let(:backup_codes) do - BackupCodeGenerator.new(user).create + BackupCodeGenerator.new(user).delete_and_regenerate end let(:payload) { { backup_code_verification_form: { backup_code: backup_codes.first } } } @@ -32,7 +32,7 @@ errors: {}, multi_factor_auth_method: 'backup_code', multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'), - new_device: nil, + new_device: true, } expect(@analytics).to receive(:track_mfa_submit_event). @@ -99,7 +99,7 @@ errors: {}, multi_factor_auth_method: 'backup_code', multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'), - new_device: nil, + new_device: true, }) expect(@irs_attempts_api_tracker).to receive(:track_event). @@ -113,37 +113,19 @@ end end - context 'with new device session value' do - it 'tracks new device value' do - freeze_time do - sign_in_before_2fa(user) - subject.user_session[:new_device] = false - stub_analytics - stub_attempts_tracker - analytics_hash = { - success: true, - errors: {}, - multi_factor_auth_method: 'backup_code', - multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'), - new_device: false, - } - - expect(@analytics).to receive(:track_mfa_submit_event). - with(analytics_hash) + context 'with existing device' do + before do + allow(controller).to receive(:new_device?).and_return(false) + end - expect(@irs_attempts_api_tracker).to receive(:track_event). - with(:mfa_login_backup_code, success: true) + it 'tracks new device value' do + stub_analytics + stub_sign_in_before_2fa(user) - post :create, params: payload + expect(@analytics).to receive(:track_mfa_submit_event). + with(hash_including(new_device: false)) - expect(subject.user_session[:auth_events]).to eq( - [ - auth_method: TwoFactorAuthenticatable::AuthMethod::BACKUP_CODE, - at: Time.zone.now, - ], - ) - expect(subject.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]).to eq false - end + post :create, params: payload end end @@ -194,7 +176,7 @@ errors: {}, multi_factor_auth_method: 'backup_code', multi_factor_auth_method_created_at: nil, - new_device: nil, + new_device: true, } stub_analytics diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 331c859a614..2cc1a037dfa 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -140,7 +140,7 @@ context: 'authentication', multi_factor_auth_method: 'sms', multi_factor_auth_method_created_at: phone_configuration_created_at.strftime('%s%L'), - new_device: nil, + new_device: true, phone_configuration_id: controller.current_user.default_phone_configuration.id, area_code: parsed_phone.area_code, country_code: parsed_phone.country, @@ -220,7 +220,7 @@ context: 'authentication', multi_factor_auth_method: 'sms', multi_factor_auth_method_created_at: phone_configuration_created_at.strftime('%s%L'), - new_device: nil, + new_device: true, phone_configuration_id: controller.current_user.default_phone_configuration.id, area_code: parsed_phone.area_code, country_code: parsed_phone.country, @@ -287,7 +287,7 @@ context: 'authentication', multi_factor_auth_method: 'sms', multi_factor_auth_method_created_at: phone_configuration_created_at.strftime('%s%L'), - new_device: nil, + new_device: true, phone_configuration_id: controller.current_user.default_phone_configuration.id, area_code: parsed_phone.area_code, country_code: parsed_phone.country, @@ -329,37 +329,21 @@ end end - context 'with new device session value' do - it 'tracks new device value' do - subject.user_session[:new_device] = false - phone_configuration_created_at = controller.current_user. - default_phone_configuration.created_at - properties = { - success: true, - confirmation_for_add_phone: false, - context: 'authentication', - multi_factor_auth_method: 'sms', - multi_factor_auth_method_created_at: phone_configuration_created_at.strftime('%s%L'), - new_device: false, - phone_configuration_id: controller.current_user.default_phone_configuration.id, - area_code: parsed_phone.area_code, - country_code: parsed_phone.country, - phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164), - enabled_mfa_methods_count: 1, - in_account_creation_flow: false, - } + context 'with existing device' do + before do + allow(controller).to receive(:new_device?).and_return(false) + end + it 'tracks new device value' do stub_analytics expect(@analytics).to receive(:track_mfa_submit_event). - with(properties) + with(hash_including(new_device: false)) - freeze_time do - post :create, params: { - code: subject.current_user.reload.direct_otp, - otp_delivery_preference: 'sms', - } - end + post :create, params: { + code: subject.current_user.reload.direct_otp, + otp_delivery_preference: 'sms', + } end end @@ -512,7 +496,7 @@ context: 'confirmation', multi_factor_auth_method: 'sms', multi_factor_auth_method_created_at: phone_configuration_created_at.strftime('%s%L'), - new_device: nil, + new_device: true, phone_configuration_id: phone_id, area_code: parsed_phone.area_code, country_code: parsed_phone.country, @@ -603,7 +587,7 @@ multi_factor_auth_method: 'sms', phone_configuration_id: controller.current_user.default_phone_configuration.id, multi_factor_auth_method_created_at: phone_configuration_created_at.strftime('%s%L'), - new_device: nil, + new_device: true, area_code: parsed_phone.area_code, country_code: parsed_phone.country, phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164), @@ -685,7 +669,7 @@ context: 'confirmation', multi_factor_auth_method: 'sms', multi_factor_auth_method_created_at: nil, - new_device: nil, + new_device: true, confirmation_for_add_phone: false, phone_configuration_id: nil, area_code: parsed_phone.area_code, diff --git a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb index 0a3a6b46ad1..2baef2c7369 100644 --- a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb @@ -56,7 +56,7 @@ multi_factor_auth_method: 'personal-key', multi_factor_auth_method_created_at: user.reload. encrypted_recovery_code_digest_generated_at.strftime('%s%L'), - new_device: nil, + new_device: true, } expect(@analytics).to receive(:track_mfa_submit_event). @@ -112,31 +112,19 @@ expect(response).to redirect_to(account_path) end end - end - context 'with new device session value' do - let(:user) { create(:user, :with_phone) } - let(:personal_key) { { personal_key: PersonalKeyGenerator.new(user).create } } - let(:payload) { { personal_key_form: personal_key } } + context 'with existing device' do + before do + allow(controller).to receive(:new_device?).and_return(false) + end - it 'tracks new device value' do - personal_key - sign_in_before_2fa(user) - stub_analytics - subject.user_session[:new_device] = false - analytics_hash = { - success: true, - errors: {}, - multi_factor_auth_method: 'personal-key', - multi_factor_auth_method_created_at: user.reload. - encrypted_recovery_code_digest_generated_at.strftime('%s%L'), - new_device: false, - } + it 'tracks new device value' do + stub_analytics + stub_sign_in_before_2fa(user) - expect(@analytics).to receive(:track_mfa_submit_event). - with(analytics_hash) + expect(@analytics).to receive(:track_mfa_submit_event). + with(hash_including(new_device: false)) - freeze_time do post :create, params: payload end end @@ -221,7 +209,7 @@ error_details: { personal_key: { personal_key_incorrect: true } }, multi_factor_auth_method: 'personal-key', multi_factor_auth_method_created_at: personal_key_generated_at.strftime('%s%L'), - new_device: nil, + new_device: true, } expect(@analytics).to receive(:track_mfa_submit_event). diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb index 520d84c26f3..e62ea8697cd 100644 --- a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb @@ -108,7 +108,7 @@ attributes = { context: 'authentication', multi_factor_auth_method: 'piv_cac', - new_device: nil, + new_device: true, piv_cac_configuration_id: nil, } @@ -120,7 +120,7 @@ errors: {}, context: 'authentication', multi_factor_auth_method: 'piv_cac', - new_device: nil, + new_device: true, multi_factor_auth_method_created_at: cfg.created_at.strftime('%s%L'), piv_cac_configuration_id: cfg.id, piv_cac_configuration_dn_uuid: cfg.x509_dn_uuid, @@ -144,27 +144,17 @@ get :show, params: { token: 'good-token' } end - context 'with new device session value' do + context 'with existing device' do before do - subject.user_session[:new_device] = false + allow(controller).to receive(:new_device?).and_return(false) end + it 'tracks new device value' do stub_analytics - cfg = controller.current_user.piv_cac_configurations.first - - submit_attributes = { - success: true, - errors: {}, - context: 'authentication', - multi_factor_auth_method: 'piv_cac', - new_device: false, - multi_factor_auth_method_created_at: cfg.created_at.strftime('%s%L'), - piv_cac_configuration_id: cfg.id, - piv_cac_configuration_dn_uuid: cfg.x509_dn_uuid, - key_id: 'foo', - } + stub_sign_in_before_2fa(user) + expect(@analytics).to receive(:track_mfa_submit_event). - with(submit_attributes) + with(hash_including(new_device: false)) get :show, params: { token: 'good-token' } end @@ -253,7 +243,7 @@ attributes = { context: 'authentication', multi_factor_auth_method: 'piv_cac', - new_device: nil, + new_device: true, piv_cac_configuration_id: nil, } @@ -271,7 +261,7 @@ context: 'authentication', multi_factor_auth_method: 'piv_cac', multi_factor_auth_method_created_at: nil, - new_device: nil, + new_device: true, key_id: 'foo', piv_cac_configuration_dn_uuid: 'bad-uuid', piv_cac_configuration_id: nil, diff --git a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb index 617078830ea..5cc253b1c51 100644 --- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb @@ -50,7 +50,7 @@ errors: {}, multi_factor_auth_method: 'totp', multi_factor_auth_method_created_at: cfg.created_at.strftime('%s%L'), - new_device: nil, + new_device: true, auth_app_configuration_id: controller.current_user.auth_app_configurations.first.id, } expect(@analytics).to receive(:track_mfa_submit_event). @@ -66,23 +66,16 @@ post :create, params: { code: generate_totp_code(@secret) } end - context 'with new device session value' do + context 'with existing device' do before do - subject.user_session[:new_device] = false + allow(controller).to receive(:new_device?).and_return(false) end + it 'tracks new device value' do - cfg = controller.current_user.auth_app_configurations.first - - attributes = { - success: true, - errors: {}, - multi_factor_auth_method: 'totp', - multi_factor_auth_method_created_at: cfg.created_at.strftime('%s%L'), - new_device: false, - auth_app_configuration_id: controller.current_user.auth_app_configurations.first.id, - } + stub_analytics + expect(@analytics).to receive(:track_mfa_submit_event). - with(attributes) + with(hash_including(new_device: false)) post :create, params: { code: generate_totp_code(@secret) } end @@ -167,7 +160,7 @@ errors: {}, multi_factor_auth_method: 'totp', multi_factor_auth_method_created_at: nil, - new_device: nil, + new_device: true, auth_app_configuration_id: nil, } diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb index 8fc1ce922e5..802b619982b 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -140,7 +140,7 @@ success: true, webauthn_configuration_id: webauthn_configuration.id, multi_factor_auth_method_created_at: webauthn_configuration.created_at.strftime('%s%L'), - new_device: nil, + new_device: true, } end @@ -175,6 +175,21 @@ end end + context 'with existing device' do + before do + allow(controller).to receive(:new_device?).and_return(false) + end + + it 'tracks new device value' do + stub_analytics + + expect(@analytics).to receive(:track_mfa_submit_event). + with(hash_including(new_device: false)) + + patch :confirm, params: params + end + end + context 'with platform authenticator' do let!(:webauthn_configuration) do create( @@ -215,42 +230,6 @@ end end - context 'with new device session value' do - let!(:webauthn_configuration) do - create( - :webauthn_configuration, - user: controller.current_user, - credential_id: credential_id, - credential_public_key: credential_public_key, - ) - controller.current_user.webauthn_configurations.first - end - let(:result) do - { - context: 'authentication', - multi_factor_auth_method: 'webauthn', - success: true, - webauthn_configuration_id: webauthn_configuration.id, - multi_factor_auth_method_created_at: webauthn_configuration.created_at.strftime('%s%L'), - new_device: false, - } - end - - before do - allow(WebauthnVerificationForm).to receive(:domain_name).and_return('localhost:3000') - subject.user_session[:new_device] = false - end - - it 'tracks new device value' do - expect(@analytics).to receive(:track_mfa_submit_event). - with(result) - - freeze_time do - patch :confirm, params: params - end - end - end - it 'tracks an invalid submission' do create( :webauthn_configuration, @@ -266,7 +245,7 @@ webauthn_configuration_id: webauthn_configuration.id, multi_factor_auth_method_created_at: webauthn_configuration.created_at. strftime('%s%L'), - new_device: nil } + new_device: true } expect(@analytics).to receive(:track_mfa_submit_event). with(result) expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) @@ -331,7 +310,7 @@ multi_factor_auth_method: 'webauthn_platform', multi_factor_auth_method_created_at: second_webauthn_platform_configuration.created_at.strftime('%s%L'), - new_device: nil, + new_device: true, webauthn_configuration_id: nil, frontend_error: webauthn_error, ) diff --git a/spec/controllers/users/backup_code_setup_controller_spec.rb b/spec/controllers/users/backup_code_setup_controller_spec.rb index e8f62a5dcf0..f0c2e12ed12 100644 --- a/spec/controllers/users/backup_code_setup_controller_spec.rb +++ b/spec/controllers/users/backup_code_setup_controller_spec.rb @@ -9,45 +9,106 @@ :confirm_user_authenticated_for_2fa_setup, :apply_secure_headers_override, [:confirm_recently_authenticated_2fa, except: ['reminder', 'continue']], - :validate_internal_referrer?, + :validate_multi_mfa_selection, ) end end - it 'creates backup codes and logs expected events' do - user = create(:user, :fully_registered) - stub_sign_in(user) - analytics = stub_analytics - stub_attempts_tracker - allow(controller).to receive(:in_multi_mfa_selection_flow?).and_return(true) - - Funnel::Registration::AddMfa.call(user.id, 'phone', analytics) - expect(PushNotification::HttpPush).to receive(:deliver). - with(PushNotification::RecoveryInformationChangedEvent.new(user: user)) - expect(@analytics).to receive(:track_event). - with('User marked authenticated', { authentication_type: :valid_2fa_confirmation }) - expect(@analytics).to receive(:track_event). - with('Backup Code Setup Visited', { + shared_examples 'valid backup codes creation' do + it 'creates backup codes and logs expected events' do + stub_analytics + stub_attempts_tracker + allow(controller).to receive(:in_multi_mfa_selection_flow?).and_return(true) + + Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics) + expect(PushNotification::HttpPush).to receive(:deliver). + with(PushNotification::RecoveryInformationChangedEvent.new(user: user)) + + response + + expect(@analytics).to have_logged_event( + 'User marked authenticated', + authentication_type: :valid_2fa_confirmation, + ) + expect(@analytics).to have_logged_event( + 'Backup Code Setup Visited', success: true, errors: {}, mfa_method_counts: { phone: 1 }, - pii_like_keypaths: [[:mfa_method_counts, :phone]], error_details: nil, enabled_mfa_methods_count: 1, in_account_creation_flow: false, - }) - expect(@analytics).to receive(:track_event). - with('Backup Code Created', { + ) + expect(@analytics).to have_logged_event( + 'Backup Code Created', enabled_mfa_methods_count: 2, in_account_creation_flow: false, - }) - expect(@irs_attempts_api_tracker).to receive(:track_event). - with(:mfa_enroll_backup_code, success: true) + ) + + expect(response).to render_template(:create) + expect(user.backup_code_configurations.length).to eq BackupCodeGenerator::NUMBER_OF_CODES + end + end + + describe '#index' do + let(:user) { create(:user, :fully_registered) } + + subject(:response) { get :index } + + before do + stub_sign_in(user) + end + + it 'redirects to setup confirmation screen' do + expect(response).to redirect_to(backup_code_confirm_setup_url) + end + + context 'in multi mfa setup flow' do + before do + allow(controller).to receive(:in_multi_mfa_selection_flow?).and_return(true) + end - get :index + it_behaves_like 'valid backup codes creation' + end + + context 'backup code confirm setup feature disabled' do + before do + allow(IdentityConfig.store).to receive(:backup_code_confirm_setup_screen_enabled). + and_return(false) + end + + it 'redirects to root url' do + expect(response).to redirect_to(root_url) + end + + context 'in multi mfa setup flow' do + before do + allow(controller).to receive(:in_multi_mfa_selection_flow?).and_return(true) + end + + it_behaves_like 'valid backup codes creation' + end + + context 'adding backup codes from account dashboard' do + before do + controller.user_session[:account_redirect_path] = account_path + end + + it_behaves_like 'valid backup codes creation' + end + end + end - expect(response).to render_template('index') - expect(user.backup_code_configurations.length).to eq BackupCodeGenerator::NUMBER_OF_CODES + describe '#create' do + let(:user) { create(:user, :fully_registered) } + + subject(:response) { post :create } + + before do + stub_sign_in(user) + end + + it_behaves_like 'valid backup codes creation' end context 'without existing backup codes' do @@ -115,14 +176,14 @@ describe 'multiple MFA handling' do let(:mfa_selections) { ['backup_code', 'voice'] } before do - @user = build(:user) + @user = create(:user) stub_sign_in(@user) controller.user_session[:mfa_selections] = mfa_selections end context 'when user selects multiple mfas on account creation' do it 'redirects to Phone Url Page after page' do - codes = BackupCodeGenerator.new(@user).create + codes = BackupCodeGenerator.new(@user).delete_and_regenerate controller.user_session[:backup_codes] = codes post :continue @@ -133,7 +194,7 @@ context 'when user only selects backup code on account creation' do let(:mfa_selections) { ['backup_code'] } it 'redirects to Suggest 2nd MFA page' do - codes = BackupCodeGenerator.new(@user).create + codes = BackupCodeGenerator.new(@user).delete_and_regenerate controller.user_session[:backup_codes] = codes post :continue expect(response).to redirect_to(auth_method_confirmation_url) @@ -143,9 +204,9 @@ context 'with multiple MFA selection turned off' do it 'redirects to account page' do - user = build(:user, :fully_registered) + user = create(:user, :fully_registered) stub_sign_in(user) - codes = BackupCodeGenerator.new(user).create + codes = BackupCodeGenerator.new(user).delete_and_regenerate controller.user_session[:backup_codes] = codes post :continue expect(response).to redirect_to(account_url) @@ -178,13 +239,4 @@ ) end end - - context 'invalid referrer to create Backup codes page' do - it 'redirects to site root' do - user = create(:user, :fully_registered) - stub_sign_in(user) - get :index - expect(response).to redirect_to(root_url) - end - end end diff --git a/spec/controllers/users/piv_cac_login_controller_spec.rb b/spec/controllers/users/piv_cac_login_controller_spec.rb index c4c80039b27..859cbdc7c45 100644 --- a/spec/controllers/users/piv_cac_login_controller_spec.rb +++ b/spec/controllers/users/piv_cac_login_controller_spec.rb @@ -61,17 +61,20 @@ }.with_indifferent_access end + subject(:response) { get :new, params: { token: } } + before do - subject.piv_session[:piv_cac_nonce] = nonce - subject.session[:sp] = sp_session + controller.piv_session[:piv_cac_nonce] = nonce + controller.session[:sp] = sp_session allow(PivCacService).to receive(:decode_token).with(token) { data } stub_analytics(user:) - get :new, params: { token: token } end context 'without a valid user' do - before do + it 'calls decode_token twice' do + response + # valid_token? is being called twice, once to determine if it's a valid submission # and once to set the session variable in process_invalid_submission # good opportunity for a refactor @@ -79,6 +82,8 @@ end it 'tracks the login attempt' do + response + expect(@analytics).to have_logged_event( :piv_cac_login, errors: { @@ -90,7 +95,9 @@ end it 'sets the session variable' do - expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to be true + response + + expect(controller.session[:needs_to_setup_piv_cac_after_sign_in]).to be true end it 'redirects to the error url' do @@ -110,12 +117,15 @@ }.with_indifferent_access end - before do + it 'calls decode_token' do + response + expect(PivCacService).to have_received(:decode_token).with(token) { data } - sign_in user end it 'tracks the login attempt' do + response + expect(@analytics).to have_logged_event( :piv_cac_login, errors: {}, @@ -125,10 +135,10 @@ end it 'sets the session correctly' do - expect(controller.user_session[:new_device]).to eq(true) + response + expect(controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]). to eq false - expect(controller.auth_methods_session.auth_events).to match( [ { @@ -139,7 +149,15 @@ ) end + it 'sets new device session value' do + expect(controller).to receive(:set_new_device_session) + + response + end + it 'tracks the user_marked_authed event' do + response + expect(@analytics).to have_logged_event( 'User marked authenticated', authentication_type: :valid_2fa, @@ -147,6 +165,8 @@ end it 'saves the piv_cac session information' do + response + session_info = { subject: data[:subject], issuer: data[:issuer], @@ -155,16 +175,6 @@ expect(controller.user_session[:decrypted_x509]).to eq session_info.to_json end - context 'from existing device' do - before do - allow(user).to receive(:new_device?).and_return(false) - end - - it 'sets the session correctly' do - expect(controller.user_session[:new_device]).to eq(true) - end - end - context 'when the user has not accepted the most recent terms of use' do let(:user) do build(:user, accepted_terms_at: IdentityConfig.store.rules_of_use_updated_at - 1.year) @@ -177,6 +187,8 @@ describe 'it handles the otp_context' do it 'tracks the user_marked_authed event' do + response + expect(@analytics).to have_logged_event( 'User marked authenticated', authentication_type: :valid_2fa, diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 49b782f7303..561746547d1 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -41,52 +41,83 @@ describe 'POST /' do include AccountResetHelper - it 'tracks the successful authentication for existing user' do - user = create(:user, :fully_registered) + context 'successful authentication' do + let(:user) { create(:user, :fully_registered) } - stub_analytics - stub_attempts_tracker - analytics_hash = { - success: true, - user_id: user.uuid, - user_locked_out: false, - bad_password_count: 0, - sp_request_url_present: false, - remember_device: false, - } + subject(:response) do + post :create, params: { user: { email: user.email, password: user.password } } + end - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) + it 'tracks the successful authentication for existing user' do + stub_analytics + stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:login_email_and_password_auth). - with(email: user.email, success: true) + response - post :create, params: { user: { email: user.email, password: user.password } } - expect(subject.session[:sign_in_flow]).to eq(:sign_in) - end + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', + success: true, + user_id: user.uuid, + user_locked_out: false, + bad_password_count: 0, + sp_request_url_present: false, + remember_device: false, + ) + end - it 'saves and refreshes cookie for device for successful authentication' do - user = create(:user, :fully_registered) + it 'assigns sign_in_flow session value' do + response + + expect(controller.session[:sign_in_flow]).to eq(:sign_in) + end - first_expires = nil + it 'sets new device session value' do + expect(controller).to receive(:set_new_device_session) - freeze_time do - post :create, params: { user: { email: user.email, password: user.password } } + response + end - device_cookie = response.headers['set-cookie'].find { |c| c.start_with?('device=') } - first_expires = CGI::Cookie.parse(device_cookie)['expires'].first - expect(Time.zone.parse(first_expires)).to be >= 20.years.from_now + it 'schedules new device alert' do + expect(UserAlerts::AlertUserAboutNewDevice).to receive(:schedule_alert) do |event:| + expect(event).to eq(user.events.where(event_type: :sign_in_before_2fa).last) + end + + response end - sign_out(user) - expect(cookies[:device]).to be_present + it 'saves and refreshes cookie for device for successful authentication' do + first_expires = nil - travel_to 10.minutes.from_now do - post :create, params: { user: { email: user.email, password: user.password } } + freeze_time do + device_cookie = response.headers['set-cookie'].find { |c| c.start_with?('device=') } + first_expires = Time.zone.parse(CGI::Cookie.parse(device_cookie)['expires'].first) + expect(first_expires).to be >= 20.years.from_now + end + + sign_out(user) + expect(cookies[:device]).to be_present + + travel_to 10.minutes.from_now do + response = post :create, params: { user: { email: user.email, password: user.password } } + + device_cookie = response.headers['set-cookie'].find { |c| c.start_with?('device=') } + second_expires = Time.zone.parse(CGI::Cookie.parse(device_cookie)['expires'].first) + expect(second_expires).to be >= first_expires + 10.minutes + end + end - device_cookie = response.headers['set-cookie'].find { |c| c.start_with?('device=') } - second_expires = CGI::Cookie.parse(device_cookie)['expires'].first - expect(Time.zone.parse(second_expires)).to be >= Time.zone.parse(first_expires) + 10.minutes + context 'with authenticated device' do + let(:user) { create(:user, :with_authenticated_device) } + + before do + request.cookies[:device] = user.devices.last.cookie_uuid + end + + it 'does not schedule new device alert' do + expect(UserAlerts::AlertUserAboutNewDevice).not_to receive(:schedule_alert) + + response + end end end diff --git a/spec/factories/devices.rb b/spec/factories/devices.rb index dded2c5574a..0ba25b7e7c1 100644 --- a/spec/factories/devices.rb +++ b/spec/factories/devices.rb @@ -8,5 +8,14 @@ last_used_at { Time.zone.now } last_ip { '127.0.0.1' } user + + trait :authenticated do + events do + [ + association(:event, event_type: :sign_in_before_2fa), + association(:event, event_type: :sign_in_after_2fa), + ] + end + end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index f3dd63cf6d6..c6005b94f6e 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -157,7 +157,8 @@ trait :with_backup_code do after :build do |user| - BackupCodeGenerator.new(user).create + user.save + BackupCodeGenerator.new(user).delete_and_regenerate end end @@ -177,6 +178,13 @@ end end + trait :with_authenticated_device do + fully_registered + after(:create) do |user| + user.devices << create(:device, :authenticated, user:) + end + end + trait :unconfirmed do confirmed_at { nil } password { nil } @@ -190,6 +198,21 @@ end end + trait :proofed_with_selfie do + fully_registered + + after :build do |user| + create( + :profile, + :active, + :verified, + :with_pii, + idv_level: :unsupervised_with_selfie, + user: user, + ) + end + end + trait :with_pending_in_person_enrollment do after :build do |user| profile = create(:profile, :with_pii, :in_person_verification_pending, user: user) diff --git a/spec/features/account/backup_codes_spec.rb b/spec/features/account/backup_codes_spec.rb index 6e614648321..b36e47f4c97 100644 --- a/spec/features/account/backup_codes_spec.rb +++ b/spec/features/account/backup_codes_spec.rb @@ -9,12 +9,14 @@ context 'with backup codes' do let(:user) { create(:user, :fully_registered, :with_piv_or_cac, :with_backup_code) } - it 'backup code generated and can be regenerated' do + it 'allows user to regenerate backup codes' do expect(page).to have_content(t('account.index.backup_codes_exist')) old_backup_code = user.backup_code_configurations.sample click_link t('forms.backup_code.regenerate'), href: backup_code_regenerate_path click_on t('account.index.backup_code_confirm_regenerate') + expect(page).to have_current_path(backup_code_setup_path) + expect(page).to have_content(t('forms.backup_code.title')) expect(BackupCodeConfiguration.where(id: old_backup_code.id).any?).to eq(false) click_continue @@ -34,23 +36,60 @@ expect(page).to have_content(t('notices.backup_codes_deleted')) expect(page).to have_current_path(account_two_factor_authentication_path) end + + context 'backup code confirm setup feature disabled' do + before do + allow(IdentityConfig.store).to receive(:backup_code_confirm_setup_screen_enabled). + and_return(false) + end + + it 'allows user to regenerate backup codes' do + expect(page).to have_content(t('account.index.backup_codes_exist')) + old_backup_code = user.backup_code_configurations.sample + click_link t('forms.backup_code.regenerate'), href: backup_code_regenerate_path + click_on t('account.index.backup_code_confirm_regenerate') + + expect(page).to have_current_path(backup_code_setup_path) + expect(page).to have_content(t('forms.backup_code.title')) + expect(BackupCodeConfiguration.where(id: old_backup_code.id).any?).to eq(false) + + click_continue + + expect(page).to have_content(t('notices.backup_codes_configured')) + expect(page).to have_current_path(account_two_factor_authentication_path) + end + end end - context 'without backup codes just phone' do + context 'without backup codes and having another mfa method' do let(:user) { create(:user, :with_phone, :with_piv_or_cac) } it 'does not show backup code section' do expect(page).to have_content(t('account.index.backup_codes_no_exist')) end - end - - context 'user clicks generate backup codes' do - let(:user) { create(:user, :with_phone, :with_piv_or_cac) } - it 'user can click generate backup codes' do + it 'allows user to create backup codes' do click_on t('forms.backup_code.generate') + # Prompt to confirm backup codes + expect(page).to have_current_path(backup_code_confirm_setup_path) + expect(page).to have_content(t('two_factor_authentication.confirm_backup_code_setup_title')) + + # Allow user to cancel + expect do + click_on t('links.cancel') + expect(page).to have_current_path(account_two_factor_authentication_path) + end.to_not change { user.backup_code_configurations.count } + + # Create codes from navigation sidebar + within('.sidenav') { click_on t('account.navigation.get_backup_codes') } + + # Allow user to confirm and continue + expect(page).to have_current_path(backup_code_confirm_setup_path) + click_continue + expect(page).to have_current_path(backup_code_setup_path) + expect(page).to have_content(t('forms.backup_code.title')) generated_at = user.backup_code_configurations. order(created_at: :asc).first.created_at. @@ -64,9 +103,36 @@ expect(page).to have_content(t('notices.backup_codes_configured')) expect(page).to have_current_path(account_two_factor_authentication_path) - expect(page).to have_content(expected_message) end + + context 'backup code confirm setup feature disabled' do + before do + allow(IdentityConfig.store).to receive(:backup_code_confirm_setup_screen_enabled). + and_return(false) + end + + it 'allows user to create backup codes' do + click_on t('forms.backup_code.generate') + + expect(page).to have_current_path(backup_code_setup_path) + + generated_at = user.backup_code_configurations. + order(created_at: :asc).first.created_at. + in_time_zone('UTC') + formatted_generated_at = l(generated_at, format: t('time.formats.event_timestamp')) + + expected_message = "#{t('account.index.backup_codes_exist')} #{formatted_generated_at}" + + expect(page).to have_current_path(backup_code_setup_path) + expect(page).to have_content(t('forms.backup_code.title')) + click_continue + + expect(page).to have_content(t('notices.backup_codes_configured')) + expect(page).to have_current_path(account_two_factor_authentication_path) + expect(page).to have_content(expected_message) + end + end end context 'with only backup codes' do diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index b8b6a03c696..7c7382c2ac3 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -78,7 +78,7 @@ }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean), 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {} + success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {}, id_issued_status: 'present', id_expiration_status: 'present' }, 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, selfie_check_required: boolean, liveness_checking_required: boolean @@ -97,7 +97,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, irs_reproofing: false, skip_hybrid_handoff: nil, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, requested_attributes: {}, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, @@ -204,7 +204,7 @@ }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'hybrid', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean), 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'hybrid', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {} + success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'hybrid', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {}, id_issued_status: 'present', id_expiration_status: 'present' }, 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, selfie_check_required: boolean, liveness_checking_required: boolean @@ -223,7 +223,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'hybrid', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, irs_reproofing: false, skip_hybrid_handoff: nil, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, requested_attributes: {}, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, @@ -327,7 +327,7 @@ }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean), 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {} + success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {}, id_issued_status: 'present', id_expiration_status: 'present' }, 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false, selfie_check_required: boolean, liveness_checking_required: boolean @@ -346,7 +346,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, irs_reproofing: false, skip_hybrid_handoff: nil, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, requested_attributes: {}, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, @@ -464,7 +464,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, irs_reproofing: false, same_address_as_id: false, skip_hybrid_handoff: nil, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: 'aaa-bbb-ccc', success: true, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: 'aaa-bbb-ccc', success: true, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, requested_attributes: {}, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', @@ -575,7 +575,7 @@ }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean), 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {} + success: true, errors: {}, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}, id_issued_status: 'present', id_expiration_status: 'present' }, 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, skip_hybrid_handoff: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, selfie_check_required: boolean, liveness_checking_required: true @@ -597,7 +597,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, irs_reproofing: false, skip_hybrid_handoff: anything, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, requested_attributes: {}, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index efb2f6e2ef5..635f942289f 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -355,6 +355,9 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.general.no_liveness')) expect(page).to have_content(resubmit_page_body_copy) @@ -404,6 +407,9 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + resubmit_page_body_copy = strip_tags( t('doc_auth.errors.general.multiple_front_id_failures'), ) @@ -455,6 +461,9 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + resubmit_page_body_copy = strip_tags( t('doc_auth.errors.general.multiple_back_id_failures'), ) @@ -502,11 +511,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.dpi.top_msg')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.dpi.top_msg')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t( @@ -522,6 +531,12 @@ inline_error_message = strip_tags(t('doc_auth.errors.dpi.failed_short')) expect(page).to have_content(inline_error_message) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.dpi.top_msg')) + expect(page).to have_content(resubmit_page_body_copy) + resubmit_page_inline_selfie_error_message = strip_tags( t('doc_auth.errors.general.selfie_failure'), ) @@ -546,15 +561,15 @@ submit_images - h1_error_message = strip_tags( + review_page_h1_copy = strip_tags( t('errors.doc_auth.selfie_not_live_or_poor_quality_heading'), ) - expect(page).to have_content(h1_error_message) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags( + review_page_body_copy = strip_tags( t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality'), ) - expect(page).to have_content(body_error_message) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 2), @@ -564,6 +579,14 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags( + t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality'), + ) + expect(page).to have_content(resubmit_page_body_copy) + resubmit_page_inline_selfie_error_message = strip_tags( t('doc_auth.errors.general.selfie_failure'), ) @@ -586,11 +609,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.dpi.top_msg')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.dpi.top_msg')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 3), @@ -600,6 +623,12 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.dpi.top_msg')) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags(t('doc_auth.errors.dpi.failed_short')) expect(page).to have_content(inline_error_message) resubmit_page_inline_selfie_error_message = strip_tags( @@ -624,11 +653,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.dpi.top_msg')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.dpi.top_msg')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 4), @@ -638,6 +667,12 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.dpi.top_msg')) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags(t('doc_auth.errors.dpi.failed_short')) expect(page).to have_content(inline_error_message) resubmit_page_inline_selfie_error_message = strip_tags( @@ -662,11 +697,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.selfie_fail_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.selfie_fail_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.general.selfie_failure')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.general.selfie_failure')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 5), @@ -676,6 +711,12 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.general.selfie_failure')) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags( t('doc_auth.errors.general.multiple_front_id_failures'), ) @@ -702,11 +743,13 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.alerts.barcode_content_check')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags( + t('doc_auth.errors.alerts.barcode_content_check'), + ) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 6), @@ -716,6 +759,14 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags( + t('doc_auth.errors.alerts.barcode_content_check'), + ) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags(t('doc_auth.errors.general.fallback_field_level')) expect(page).to have_content(inline_error_message) resubmit_page_inline_selfie_error_message = strip_tags( @@ -740,11 +791,13 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.alerts.barcode_content_check')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags( + t('doc_auth.errors.alerts.barcode_content_check'), + ) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 7), @@ -754,6 +807,14 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags( + t('doc_auth.errors.alerts.barcode_content_check'), + ) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags(t('doc_auth.errors.general.fallback_field_level')) expect(page).to have_content(inline_error_message) resubmit_page_inline_selfie_error_message = strip_tags( @@ -778,11 +839,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.general.no_liveness')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.general.no_liveness')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 8), @@ -792,6 +853,12 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.general.no_liveness')) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags(t('doc_auth.errors.general.fallback_field_level')) expect(page).to have_content(inline_error_message) resubmit_page_inline_selfie_error_message = strip_tags( @@ -816,15 +883,15 @@ submit_images - h1_error_message = strip_tags( + review_page_h1_copy = strip_tags( t('errors.doc_auth.selfie_not_live_or_poor_quality_heading'), ) - expect(page).to have_content(h1_error_message) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags( + review_page_body_copy = strip_tags( t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality'), ) - expect(page).to have_content(body_error_message) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 9), @@ -834,6 +901,14 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags( + t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality'), + ) + expect(page).to have_content(resubmit_page_body_copy) + resubmit_page_inline_selfie_error_message = strip_tags( t('doc_auth.errors.general.selfie_failure'), ) @@ -856,11 +931,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.rate_limited_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.rate_limited_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.alerts.address_check')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.alerts.address_check')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 10), @@ -870,6 +945,12 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.alerts.address_check')) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags( t('doc_auth.errors.general.multiple_front_id_failures'), ) @@ -896,11 +977,11 @@ submit_images - h1_error_message = strip_tags(t('errors.doc_auth.selfie_fail_heading')) - expect(page).to have_content(h1_error_message) + review_page_h1_copy = strip_tags(t('errors.doc_auth.selfie_fail_heading')) + expect(page).to have_content(review_page_h1_copy) - body_error_message = strip_tags(t('doc_auth.errors.general.selfie_failure')) - expect(page).to have_content(body_error_message) + review_page_body_copy = strip_tags(t('doc_auth.errors.general.selfie_failure')) + expect(page).to have_content(review_page_body_copy) review_issues_rate_limit_warning = strip_tags( t('idv.failure.attempts_html', count: max_attempts - 11), @@ -910,6 +991,12 @@ click_try_again expect(page).to have_current_path(idv_document_capture_path) + resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues')) + expect(page).to have_content(resubmit_page_h1_copy) + + resubmit_page_body_copy = strip_tags(t('doc_auth.errors.general.selfie_failure')) + expect(page).to have_content(resubmit_page_body_copy) + inline_error_message = strip_tags( t('doc_auth.errors.general.multiple_front_id_failures'), ) diff --git a/spec/features/new_device_tracking_spec.rb b/spec/features/new_device_tracking_spec.rb index 7f300f26f1f..49ebc7bd324 100644 --- a/spec/features/new_device_tracking_spec.rb +++ b/spec/features/new_device_tracking_spec.rb @@ -6,11 +6,11 @@ let(:user) { create(:user, :fully_registered) } context 'user has existing devices and aggregated new device alerts is disabled' do + let(:user) { create(:user, :with_authenticated_device) } before do allow(IdentityConfig.store).to receive( :feature_new_device_alert_aggregation_enabled, ).and_return(false) - create(:device, user: user) end it 'sends a user notification on signin' do @@ -46,11 +46,11 @@ end context 'user has existing devices and aggregated new device alerts is enabled' do + let(:user) { create(:user, :with_authenticated_device) } before do allow(IdentityConfig.store).to receive( :feature_new_device_alert_aggregation_enabled, ).and_return(true) - create(:device, user: user) end it 'sends a user notification on signin' do @@ -64,6 +64,63 @@ ) end + it 'sends all notifications for an expired sign-in session' do + allow(IdentityConfig.store).to receive(:new_device_alert_delay_in_minutes).and_return(5) + allow(IdentityConfig.store).to receive(:session_timeout_warning_seconds).and_return(15) + + sign_in_user(user) + + # Notified after expired delay for successful email password, but incomplete MFA + travel_to 6.minutes.from_now do + CreateNewDeviceAlert.new.perform(Time.zone.now) + open_last_email + email_page = Capybara::Node::Simple.new(current_email.default_part_body) + expect(email_page).to have_css( + '.usa-table td.font-family-mono', + count: 1, + text: t('user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa'), + ) + end + + reset_email + + travel_to 16.minutes.from_now do + visit root_url + expect(current_path).to eq(new_user_session_path) + sign_in_user(user) + end + + # Notified after session expired, user returned for another successful email password, no MFA + travel_to 22.minutes.from_now do + CreateNewDeviceAlert.new.perform(Time.zone.now) + open_last_email + email_page = Capybara::Node::Simple.new(current_email.default_part_body) + expect(email_page).to have_css( + '.usa-table td.font-family-mono', + count: 1, + text: t('user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa'), + ) + end + + reset_email + + # Notified after session expired, user returned for successful email password and MFA + travel_to 38.minutes.from_now do + visit root_url + expect(current_path).to eq(new_user_session_path) + sign_in_live_with_2fa(user) + open_last_email + email_page = Capybara::Node::Simple.new(current_email.default_part_body) + expect(email_page).to have_css('.usa-table td.font-family-mono', count: 2) + expect(email_page).to have_content( + t('user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa'), + ) + expect(email_page).to have_content( + t('user_mailer.new_device_sign_in_attempts.events.sign_in_after_2fa'), + ) + end + end + context 'from existing device' do before do Capybara.current_session.driver.browser.current_session.cookie_jar[:device] = diff --git a/spec/features/sign_in/multiple_vot_spec.rb b/spec/features/sign_in/multiple_vot_spec.rb new file mode 100644 index 00000000000..80f4bbf59d7 --- /dev/null +++ b/spec/features/sign_in/multiple_vot_spec.rb @@ -0,0 +1,296 @@ +require 'rails_helper' + +RSpec.feature 'Sign in with multiple vectors of trust', allowed_extra_analytics: [:*] do + include SamlAuthHelper + include OidcAuthHelper + include IdvHelper + include DocAuthHelper + + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture_enabled).and_return(true) + end + + context 'with OIDC' do + context 'biometric and non-biometric proofing is acceptable' do + scenario 'identity proofing is not required if user is proofed with biometric' do + user = create(:user, :proofed_with_selfie) + + visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info + + expect(user_info[:given_name]).to be_present + expect(user_info[:vot]).to eq('C1.C2.P1.Pb') + end + + scenario 'identity proofing is not required if user is proofed without biometric' do + user = create(:user, :proofed) + + visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info + + expect(user_info[:given_name]).to be_present + expect(user_info[:vot]).to eq('C1.C2.P1') + end + + scenario 'identity proofing with biometric is required if user is not proofed', :js do + user = create(:user, :fully_registered) + + visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(idv_welcome_path) + complete_all_doc_auth_steps_before_password_step(with_selfie: true) + fill_in 'Password', with: user.password + click_continue + acknowledge_and_confirm_personal_key + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info + + expect(user_info[:given_name]).to be_present + expect(user_info[:vot]).to eq('C1.C2.P1.Pb') + end + end + + context 'proofing or no proofing is acceptable (IALMAX)' do + scenario 'identity proofing is not required if the user is not proofed' do + user = create(:user, :fully_registered) + + visit_idp_from_oidc_sp_with_vtr( + vtr: ['C1.C2.P1', 'C1.C2'], + scope: 'openid email profile:name', + ) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info + + expect(user_info[:given_name]).to_not be_present + expect(user_info[:vot]).to eq('C1.C2') + end + + scenario 'attributes are shared if the user is proofed' do + user = create(:user, :proofed) + + visit_idp_from_oidc_sp_with_vtr( + vtr: ['C1.C2.P1', 'C1.C2'], + scope: 'openid email profile:name', + ) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info + + expect(user_info[:given_name]).to be_present + expect(user_info[:vot]).to eq('C1.C2.P1') + end + + scenario 'identity proofing is not required if proofed user resets password' do + user = create(:user, :proofed) + + visit_idp_from_oidc_sp_with_vtr( + vtr: ['C1.C2.P1', 'C1.C2'], + scope: 'openid email profile:name', + ) + trigger_reset_password_and_click_email_link(user.email) + reset_password(user, 'new even better password') + user.password = 'new even better password' + sign_in_live_with_2fa(user) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info + + expect(user_info[:given_name]).to_not be_present + expect(user_info[:vot]).to eq('C1.C2') + end + end + end + + context 'with SAML' do + before do + if javascript_enabled? + service_provider = ServiceProvider.find_by(issuer: sp1_issuer) + acs_url = URI.parse(service_provider.acs_url) + acs_url.host = page.server.host + acs_url.port = page.server.port + service_provider.update(acs_url: acs_url.to_s) + end + end + + context 'biometric and non-biometric proofing is acceptable' do + scenario 'identity proofing is not required if user is proofed with biometric' do + user = create(:user, :proofed_with_selfie) + + visit_saml_authn_request_url( + overrides: { issuer: sp1_issuer, authn_context: ['C1.C2.P1.Pb', 'C1.C2.P1'] }, + ) + sign_in_live_with_2fa(user) + + click_submit_default + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2.P1.Pb') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2.P1.Pb') + + first_name = xmldoc.attribute_node_for('first_name').content + expect(first_name).to_not be_blank + end + + scenario 'identity proofing is not required if user is proofed without biometric' do + user = create(:user, :proofed) + + visit_saml_authn_request_url( + overrides: { issuer: sp1_issuer, authn_context: ['C1.C2.P1.Pb', 'C1.C2.P1'] }, + ) + sign_in_live_with_2fa(user) + + click_submit_default + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2.P1') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2.P1') + + first_name = xmldoc.attribute_node_for('first_name').content + expect(first_name).to_not be_blank + end + + scenario 'identity proofing with biometric is required if user is not proofed', :js do + user = create(:user, :fully_registered) + + visit_saml_authn_request_url( + overrides: { issuer: sp1_issuer, authn_context: ['C1.C2.P1.Pb', 'C1.C2.P1'] }, + ) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(idv_welcome_path) + complete_all_doc_auth_steps_before_password_step(with_selfie: true) + fill_in 'Password', with: user.password + click_continue + acknowledge_and_confirm_personal_key + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2.P1.Pb') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2.P1.Pb') + + first_name = xmldoc.attribute_node_for('first_name').content + expect(first_name).to_not be_blank + end + end + + context 'proofing or no proofing is acceptable (IALMAX)' do + scenario 'identity proofing is not required if the user is not proofed' do + user = create(:user, :fully_registered) + + visit_saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: [ + 'C1.C2.P1', + 'C1.C2', + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name", + ], + }, + ) + sign_in_live_with_2fa(user) + + click_submit_default + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2') + + first_name_node = xmldoc.attribute_node_for('first_name') + expect(first_name_node).to be_nil + end + + scenario 'attributes are shared if the user is proofed' do + user = create(:user, :proofed) + + visit_saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: [ + 'C1.C2.P1', + 'C1.C2', + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name", + ], + }, + ) + sign_in_live_with_2fa(user) + + click_submit_default + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2.P1') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2.P1') + + first_name = xmldoc.attribute_node_for('first_name').content + expect(first_name).to_not be_blank + end + + scenario 'identity proofing is not required if proofed user resets password' do + user = create(:user, :proofed) + + visit_saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: [ + 'C1.C2.P1', + 'C1.C2', + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name", + ], + }, + ) + trigger_reset_password_and_click_email_link(user.email) + reset_password(user, 'new even better password') + user.password = 'new even better password' + sign_in_live_with_2fa(user) + + click_submit_default + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2') + + first_name_node = xmldoc.attribute_node_for('first_name') + expect(first_name_node).to be_nil + end + end + end +end diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb index e4a2f43cde5..640365d0f6f 100644 --- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb @@ -42,7 +42,7 @@ it 'works for each code and refreshes the codes on the last one' do user = create(:user, :fully_registered, :with_authentication_app) - codes = BackupCodeGenerator.new(user).create + codes = BackupCodeGenerator.new(user).delete_and_regenerate BackupCodeGenerator::NUMBER_OF_CODES.times do |index| signin(user.email, user.password) diff --git a/spec/fixtures/proofing/aamva/requests/verification_request.xml b/spec/fixtures/proofing/aamva/requests/verification_request.xml index 3e6068d01db..8321a8aba2c 100644 --- a/spec/fixtures/proofing/aamva/requests/verification_request.xml +++ b/spec/fixtures/proofing/aamva/requests/verification_request.xml @@ -1,37 +1,37 @@ - + http://aamva.org/dldv/wsdl/2.1/IDLDVService21/VerifyDriverLicenseData - - + + KEYKEYKEY - - - - - + + + + + 1234-abcd-efgh - - GSA - CA - - - - 123456789 - - 1942-10-29 - - Testy - McTesterson - - - 123 Sunnyside way - Sterling - VA - 20176 - - - + + GSA + CA + + + + 123456789 + + 1942-10-29 + + Testy + McTesterson + + + 123 Sunnyside way + Sterling + VA + 20176 + + + diff --git a/spec/forms/backup_code_verification_form_spec.rb b/spec/forms/backup_code_verification_form_spec.rb index 040189f5c14..a59ab627f87 100644 --- a/spec/forms/backup_code_verification_form_spec.rb +++ b/spec/forms/backup_code_verification_form_spec.rb @@ -4,7 +4,7 @@ subject(:result) { described_class.new(user).submit(params).to_h } let(:user) { create(:user) } - let(:backup_codes) { BackupCodeGenerator.new(user).create } + let(:backup_codes) { BackupCodeGenerator.new(user).delete_and_regenerate } let(:backup_code_config) do BackupCodeConfiguration.find_with_code(code: code, user_id: user.id) end diff --git a/spec/forms/idv/doc_pii_form_spec.rb b/spec/forms/idv/doc_pii_form_spec.rb index 1f6ba9f45f0..b955572a395 100644 --- a/spec/forms/idv/doc_pii_form_spec.rb +++ b/spec/forms/idv/doc_pii_form_spec.rb @@ -19,6 +19,8 @@ state: Faker::Address.state_abbr, state_id_jurisdiction: 'AL', state_id_number: 'S59397998', + state_id_issued: '2024-01-01', + state_id_expiration: '2024-01-01', } end let(:name_errors_pii) do @@ -138,6 +140,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'present', + id_expiration_status: 'present', ) end end @@ -154,6 +158,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -176,6 +182,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -194,6 +202,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -212,6 +222,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -230,6 +242,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end @@ -256,6 +270,8 @@ expect(result.extra).to eq( attention_with_barcode: false, pii_like_keypaths: pii_like_keypaths, + id_issued_status: 'missing', + id_expiration_status: 'missing', ) end end diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 7b7bedbcb25..44b54a8526e 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -416,160 +416,6 @@ end end - describe '#ial' do - context 'with vtr param' do - let(:acr_values) { nil } - - context 'when proofing is requested' do - let(:vtr) { ['C1.P1'].to_json } - - it { expect(form.ial).to eq(2) } - end - - context 'when proofing is not requested' do - let(:vtr) { ['C1'].to_json } - - it { expect(form.ial).to eq(1) } - end - end - - context 'with acr_values param' do - let(:vtr) { nil } - - context 'when IAL1 passed' do - let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } - - it 'returns 1' do - expect(form.ial).to eq(1) - end - end - - context 'when IAL2 passed' do - let(:acr_values) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } - - it 'returns 2' do - expect(form.ial).to eq(2) - end - end - - context 'when IALMAX passed' do - let(:acr_values) { Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } - - it 'returns 0' do - expect(form.ial).to eq(0) - end - end - - context 'when LOA1 passed' do - let(:acr_values) { Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF } - - it 'returns 1' do - expect(form.ial).to eq(1) - end - end - - context 'when LOA3 passed' do - let(:acr_values) { Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF } - - it 'returns 2' do - expect(form.ial).to eq(2) - end - end - end - end - - describe '#aal' do - context 'with vtr param' do - let(:acr_values) { nil } - - context 'when AAL2 is requested' do - let(:vtr) { ['C2'].to_json } - - it { expect(form.aal).to eq(2) } - end - - context 'when AAL2 is not requested' do - let(:vtr) { ['C1'].to_json } - - it { expect(form.aal).to eq(1) } - end - end - - context 'with acr_values param' do - let(:vtr) { nil } - - context 'when no AAL passed' do - let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } - - it 'returns 0' do - expect(form.aal).to eq(0) - end - end - - context 'when DEFAULT_AAL passed' do - let(:acr_values) { Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF } - - it 'returns 0' do - expect(form.aal).to eq(0) - end - end - - context 'when AAL2 passed' do - let(:acr_values) { Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF } - - it 'returns 2' do - expect(form.aal).to eq(2) - end - end - - context 'when AAL2_PHISHING_RESISTANT passed' do - let(:acr_values) { Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF } - - it 'returns 2' do - expect(form.aal).to eq(2) - end - end - - context 'when AAL2_HSPD12 passed' do - let(:acr_values) { Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF } - - it 'returns 2' do - expect(form.aal).to eq(2) - end - end - - context 'when AAL3 passed' do - let(:acr_values) { Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF } - - it 'returns 3' do - expect(form.aal).to eq(3) - end - end - - context 'when AAL3_HSPD12 passed' do - let(:acr_values) { Saml::Idp::Constants::AAL3_HSPD12_AUTHN_CONTEXT_CLASSREF } - - it 'returns 3' do - expect(form.aal).to eq(3) - end - end - - context 'when IAL and AAL passed' do - aal2 = Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF - ial2 = Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF - - let(:acr_values) do - "#{aal2} #{ial2}" - end - - it 'returns ial and aal' do - expect(form.aal).to eq(2) - expect(form.ial).to eq(2) - end - end - end - end - describe '#requested_aal_value' do context 'with ACR values' do let(:vtr) { nil } @@ -775,7 +621,11 @@ let(:code_challenge_method) { 'S256' } it 'records the code_challenge on the identity' do - form.link_identity_to_service_provider(user, rails_session_id) + form.link_identity_to_service_provider( + current_user: user, + ial: 1, + rails_session_id: rails_session_id, + ) identity = user.identities.where(service_provider: client_id).first @@ -794,7 +644,11 @@ context 'when the identity has been linked' do before do - form.link_identity_to_service_provider(user, rails_session_id) + form.link_identity_to_service_provider( + current_user: user, + ial: 1, + rails_session_id: rails_session_id, + ) end it 'returns a redirect URI with the code from the identity session_uuid' do @@ -818,9 +672,9 @@ end end - describe '#biometric_comparison_required?' do + describe '#biometric_comparison_requested?' do it 'returns false by default' do - expect(subject.biometric_comparison_required?).to eql(false) + expect(subject.biometric_comparison_requested?).to eql(false) end context 'biometric requested via VTR' do @@ -828,7 +682,7 @@ let(:vtr) { ['C1.P1.Pb'].to_json } it 'returns true' do - expect(subject.biometric_comparison_required?).to eql(true) + expect(subject.biometric_comparison_requested?).to eql(true) end end @@ -837,7 +691,16 @@ let(:vtr) { ['C1.P1'].to_json } it 'returns false' do - expect(subject.biometric_comparison_required?).to eql(false) + expect(subject.biometric_comparison_requested?).to eql(false) + end + end + + context 'multiple VTR including biometric comparison' do + let(:acr_values) { nil } + let(:vtr) { ['C1.P1', 'C1.P1.Pb'].to_json } + + it 'returns false' do + expect(subject.biometric_comparison_requested?).to eql(true) end end end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 2535db1ae88..84c4b241df4 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -89,32 +89,17 @@ class BaseTask { key: 'anonymous_mailer.password_reset_missing_user.try_different_email', locales: %i[zh] }, { key: 'anonymous_mailer.password_reset_missing_user.use_this_email_html', locales: %i[zh] }, { key: 'doc_auth.buttons.close', locales: %i[zh] }, - { key: 'doc_auth.errors.alerts.selfie_not_live_help_link_text', locales: %i[zh] }, - { key: 'doc_auth.errors.alerts.selfie_not_live_or_poor_quality', locales: %i[zh] }, - { key: 'doc_auth.errors.general.selfie_failure_help_link_text', locales: %i[zh] }, - { key: 'doc_auth.headings.hybrid_handoff_selfie', locales: %i[zh] }, { key: 'doc_auth.info.getting_started_html', locales: %i[zh] }, { key: 'doc_auth.info.getting_started_learn_more', locales: %i[zh] }, - { key: 'doc_auth.info.hybrid_handoff_ipp_html', locales: %i[zh] }, - { key: 'doc_auth.info.selfie_capture_content', locales: %i[zh] }, - { key: 'doc_auth.info.selfie_capture_status.face_close_to_border', locales: %i[zh] }, - { key: 'doc_auth.info.selfie_capture_status.face_not_found', locales: %i[zh] }, - { key: 'doc_auth.info.selfie_capture_status.face_too_small', locales: %i[zh] }, - { key: 'doc_auth.info.selfie_capture_status.too_many_faces', locales: %i[zh] }, { key: 'doc_auth.info.stepping_up_html', locales: %i[zh] }, { key: 'doc_auth.instructions.bullet4', locales: %i[zh] }, { key: 'doc_auth.instructions.getting_started', locales: %i[zh] }, { key: 'doc_auth.instructions.text3', locales: %i[zh] }, { key: 'doc_auth.instructions.text4', locales: %i[zh] }, - { key: 'doc_auth.tips.document_capture_selfie_text4', locales: %i[zh] }, { key: 'errors.doc_auth.document_capture_canceled', locales: %i[zh] }, - { key: 'errors.doc_auth.selfie_fail_heading', locales: %i[zh] }, - { key: 'errors.doc_auth.selfie_not_live_or_poor_quality_heading', locales: %i[zh] }, { key: 'errors.messages.blank_cert_element_req', locales: %i[zh] }, { key: 'event_types.sign_in_notification_timeframe_expired', locales: %i[zh] }, { key: 'event_types.sign_in_unsuccessful_2fa', locales: %i[zh] }, - { key: 'forms.buttons.continue_ipp', locales: %i[zh] }, - { key: 'forms.buttons.continue_remote', locales: %i[zh] }, { key: 'forms.webauthn_setup.learn_more', locales: %i[zh] }, { key: 'forms.webauthn_setup.step_1', locales: %i[zh] }, { key: 'forms.webauthn_setup.step_1a', locales: %i[zh] }, @@ -159,22 +144,14 @@ class BaseTask { key: 'two_factor_authentication.webauthn_roaming.nickname', locales: %i[zh] }, { key: 'two_factor_authentication.webauthn_roaming.renamed', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_after_2fa.authentication_methods', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_after_2fa.info_p1', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_after_2fa.info_p2', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_after_2fa.info_p3_html', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_after_2fa.reset_password', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_after_2fa.subject', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_attempts.events.sign_in_after_2fa', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_attempts.events.sign_in_unsuccessful_2fa', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_attempts.new_sign_in_from', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_before_2fa.info_p1_html.one', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_before_2fa.info_p1_html.other', locales: %i[zh] }, { key: 'user_mailer.new_device_sign_in_before_2fa.info_p1_html.zero', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_before_2fa.info_p2', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_before_2fa.info_p3_html', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_before_2fa.reset_password', locales: %i[zh] }, - { key: 'user_mailer.new_device_sign_in_before_2fa.subject', locales: %i[zh] }, { key: 'openid_connect.authorization.errors.bad_client_id', locales: %i[zh] }, { key: 'openid_connect.authorization.errors.invalid_verified_within_duration.one', locales: %i[zh] }, { key: 'openid_connect.authorization.errors.invalid_verified_within_duration.other', locales: %i[zh] }, diff --git a/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx index e862a1fcd79..0a54a25f1af 100644 --- a/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx @@ -27,3 +27,47 @@ it('shows the Acuant div when the script has loaded', () => { expect(container.querySelector('#acuant-face-capture-container')).to.exist(); expect(container.querySelector('.acuant-capture-canvas__spinner')).not.to.exist(); }); + +it('shows the fullscreen close button before acuant is hydrated in', () => { + // Render the AcuantSelfieCaptureCanvas component with some text + const imageCaptureText = 'Face not found'; + const { rerender, container, queryByRole } = render( + + + + + , + ); + // Check that the button exists + expect(queryByRole('button')).to.exist(); + + // Mock how Acuant sets up the dom by creating this structure of divs + // '#acuant-face-capture-container>#acuant-face-capture-camera>#cameraContainer' + const acuantFaceCaptureDiv = document.createElement('div'); + acuantFaceCaptureDiv.id = 'acuant-face-capture-camera'; + const acuantFaceCaptureContainer = container.querySelector('#acuant-face-capture-container'); + acuantFaceCaptureContainer.appendChild(acuantFaceCaptureDiv); + expect( + container.querySelector('#acuant-face-capture-container>#acuant-face-capture-camera'), + ).to.exist(); + + // Mock how Acuant sets up the shadow dom with the #cameraContainer div inside it + const cameraContainer = document.createElement('div'); + cameraContainer.id = 'cameraContainer'; + const shadow = container + .querySelector('#acuant-face-capture-camera') + .attachShadow({ mode: 'open' }); + shadow.appendChild(cameraContainer); + + // Rerender the component, the shadow dom continues to exist + const newImageCaptureText = 'Too many faces'; + rerender( + + + + + , + ); + // The button now disappears + expect(queryByRole('button')).not.to.exist(); +}); diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index d9c7665997e..924004c1c21 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -211,6 +211,8 @@ and_return(reprocess_delay_minutes) allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx). and_return(in_person_proofing_enforce_tmx) + allow(IdentityConfig.store).to receive(:in_person_enrollments_ready_job_enabled). + and_return(false) stub_const( 'GetUspsProofingResultsJob::REQUEST_DELAY_IN_SECONDS', request_delay_ms / GetUspsProofingResultsJob::MILLISECONDS_PER_SECOND, diff --git a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb index f42fe8a4e99..31b075152ff 100644 --- a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb +++ b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb @@ -88,6 +88,7 @@ context 'with data' do let(:user1) { create(:user) } let(:user2) { create(:user) } + let(:user3) { create(:user) } before do iaa_order1.integrations << build_integration( @@ -124,21 +125,23 @@ issuer: iaa2_sp1.issuer, requested_at: inside_iaa2, returned_at: inside_iaa2, - profile_verified_at: '2019-01-01 00:00:00', - billable: true, - ) - - # 1 unique user in month 1 at IAA 2 sp 2 @ IAL 2 with profile age 2 - create( - :sp_return_log, - user_id: user2.id, - ial: 2, - issuer: iaa2_sp2.issuer, - requested_at: inside_iaa2, - returned_at: inside_iaa2, profile_verified_at: '2020-01-01 00:00:00', billable: true, ) + + # 2 unique user in month 1 at IAA 2 sp 2 @ IAL 2 with profile age 2 + [user2, user3].each do |user| + create( + :sp_return_log, + user_id: user.id, + ial: 2, + issuer: iaa2_sp2.issuer, + requested_at: inside_iaa2, + returned_at: inside_iaa2, + profile_verified_at: '2019-01-01 00:00:00', + billable: true, + ) + end end it 'generates a report by iaa + order number and issuer and year month' do @@ -177,44 +180,95 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(1) expect(row['issuer_ial2_unique_users'].to_i).to eq(0) expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year_unknown'].to_i).to eq(0) + end + + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa2_sp1.issuer } + + expect(row['iaa_order_number']).to eq('gtc5678-0002') + expect(row['partner']).to eq(partner_account2.requesting_agency) + expect(row['iaa_start_date']).to eq('2020-09-01') + expect(row['iaa_end_date']).to eq('2021-08-30') + + expect(row['issuer']).to eq(iaa2_sp1.issuer) + expect(row['friendly_name']).to eq(iaa2_sp1.friendly_name) + + expect(row['year_month']).to eq('202009') + expect(row['year_month_readable']).to eq('September 2020') + + expect(row['iaa_ial1_unique_users'].to_i).to eq(0) + expect(row['iaa_ial2_unique_users'].to_i).to eq(3) + expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(3) + expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(2) + expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year_unknown'].to_i).to eq(0) + + expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) + expect(row['issuer_ial2_total_auth_count'].to_i).to eq(1) + expect(row['issuer_ial1_plus_2_total_auth_count'].to_i).to eq(1) + + expect(row['issuer_ial1_unique_users'].to_i).to eq(0) + expect(row['issuer_ial2_unique_users'].to_i).to eq(1) + expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year_unknown'].to_i).to eq(0) end - [iaa2_sp1, iaa2_sp2].each do |sp| - aggregate_failures do - row = csv.find { |r| r['issuer'] == sp.issuer } - - expect(row['iaa_order_number']).to eq('gtc5678-0002') - expect(row['partner']).to eq(partner_account2.requesting_agency) - expect(row['iaa_start_date']).to eq('2020-09-01') - expect(row['iaa_end_date']).to eq('2021-08-30') - - expect(row['issuer']).to eq(sp.issuer) - expect(row['friendly_name']).to eq(sp.friendly_name) - - expect(row['year_month']).to eq('202009') - expect(row['year_month_readable']).to eq('September 2020') - - expect(row['iaa_ial1_unique_users'].to_i).to eq(0) - expect(row['iaa_ial2_unique_users'].to_i).to eq(2) - expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(2) - expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year_unknown'].to_i).to eq(0) - - expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) - expect(row['issuer_ial2_total_auth_count'].to_i).to eq(1) - expect(row['issuer_ial1_plus_2_total_auth_count'].to_i).to eq(1) - - expect(row['issuer_ial1_unique_users'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users'].to_i).to eq(1) - expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users'].to_i).to eq(1) - end + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa2_sp2.issuer } + + expect(row['iaa_order_number']).to eq('gtc5678-0002') + expect(row['partner']).to eq(partner_account2.requesting_agency) + expect(row['iaa_start_date']).to eq('2020-09-01') + expect(row['iaa_end_date']).to eq('2021-08-30') + + expect(row['issuer']).to eq(iaa2_sp2.issuer) + expect(row['friendly_name']).to eq(iaa2_sp2.friendly_name) + + expect(row['year_month']).to eq('202009') + expect(row['year_month_readable']).to eq('September 2020') + + expect(row['iaa_ial1_unique_users'].to_i).to eq(0) + expect(row['iaa_ial2_unique_users'].to_i).to eq(3) + expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(3) + expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(2) + expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_users_year_unknown'].to_i).to eq(0) + + expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) + expect(row['issuer_ial2_total_auth_count'].to_i).to eq(2) + expect(row['issuer_ial1_plus_2_total_auth_count'].to_i).to eq(2) + + expect(row['issuer_ial1_unique_users'].to_i).to eq(0) + expect(row['issuer_ial2_unique_users'].to_i).to eq(2) + expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(2) + expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(2) + expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_users_year_unknown'].to_i).to eq(0) end end end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 639d69011fd..c41649e4b7c 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -88,7 +88,7 @@ expect(result_context_stages_state_id[:success]).to eq(true) expect(result_context_stages_state_id[:timed_out]).to eq(false) expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh') - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) @@ -165,7 +165,7 @@ expect(result_context_stages_state_id[:success]).to eq(true) expect(result_context_stages_state_id[:timed_out]).to eq(false) expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh') - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) @@ -249,7 +249,7 @@ # result[:context][:stages][:state_id] expect(result_context_stages_state_id[:vendor_name]).to eq('aamva:state_id') expect(result_context_stages_state_id[:success]).to eq(true) - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) end @@ -490,7 +490,7 @@ expect(result_context_stages_state_id[:success]).to eq(true) expect(result_context_stages_state_id[:timed_out]).to eq(false) expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh') - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) diff --git a/spec/lib/aamva_test_spec.rb b/spec/lib/aamva_test_spec.rb index e490bd113ba..4a6e5e399ba 100644 --- a/spec/lib/aamva_test_spec.rb +++ b/spec/lib/aamva_test_spec.rb @@ -43,7 +43,7 @@ expect(WebMock).to( have_requested(:post, verification_url).with do |req| - expect(Nokogiri::XML(req.body).at_xpath('//ns1:MessageDestinationId').text). + expect(Nokogiri::XML(req.body).at_xpath('//aa:MessageDestinationId').text). to eq('P6'), 'it sends a request with the designated fake state' end, ) diff --git a/spec/models/backup_code_configuration_spec.rb b/spec/models/backup_code_configuration_spec.rb index dc0c3787975..05114d8473d 100644 --- a/spec/models/backup_code_configuration_spec.rb +++ b/spec/models/backup_code_configuration_spec.rb @@ -24,7 +24,7 @@ it 'is truthy if there is a backup code configuration event' do user = User.new user.save - BackupCodeGenerator.new(user).create + BackupCodeGenerator.new(user).delete_and_regenerate user.backup_code_configurations.each do |backup_code_config| expect(backup_code_config.mfa_enabled?).to be_truthy @@ -69,21 +69,21 @@ let(:user) { create(:user) } it 'returns the code' do - codes = BackupCodeGenerator.new(user).create + codes = BackupCodeGenerator.new(user).delete_and_regenerate first_code = codes.first expect(BackupCodeConfiguration.find_with_code(code: first_code, user_id: user.id)).to be end it 'does not return the code with a wrong user id' do - codes = BackupCodeGenerator.new(user).create + codes = BackupCodeGenerator.new(user).delete_and_regenerate first_code = codes.first expect(BackupCodeConfiguration.find_with_code(code: first_code, user_id: 1234)).to be_nil end it 'finds codes via salted_code_fingerprint' do - codes = BackupCodeGenerator.new(user).create + codes = BackupCodeGenerator.new(user).delete_and_regenerate first_code = codes.first backup_code = BackupCodeConfiguration.find_with_code(code: first_code, user_id: user.id) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 72f86878ae8..67798ff36d4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1589,6 +1589,42 @@ def it_should_not_send_survey end end + describe '#authenticated_device?' do + let(:user) { create(:user, :fully_registered) } + let(:device) { create(:device, user:) } + let(:cookie_uuid) { device.cookie_uuid } + subject(:result) { user.authenticated_device?(cookie_uuid:) } + + context 'with blank cookie uuid' do + let(:cookie_uuid) { nil } + + it { expect(result).to eq(false) } + end + + context 'with cookie uuid not matching user device' do + let(:cookie_uuid) { 'invalid' } + + it { expect(result).to eq(false) } + end + + context 'with existing device without sign_in_after_2fa event' do + before do + create(:event, device:, event_type: :sign_in_before_2fa) + end + + it { expect(result).to eq(false) } + end + + context 'with existing device with sign_in_after_2fa event' do + before do + create(:event, device:, event_type: :sign_in_before_2fa) + create(:event, device:, event_type: :sign_in_after_2fa) + end + + it { expect(result).to eq(true) } + end + end + describe '#password_reset_profile' do let(:user) { create(:user) } diff --git a/spec/policies/pending_profile_policy_spec.rb b/spec/policies/pending_profile_policy_spec.rb index 37e68992e66..dfe6df763ca 100644 --- a/spec/policies/pending_profile_policy_spec.rb +++ b/spec/policies/pending_profile_policy_spec.rb @@ -4,6 +4,7 @@ let(:user) { create(:user) } let(:resolved_authn_context_result) do AuthnContextResolver.new( + user: user, service_provider: nil, vtr: vtr, acr_values: acr_values, diff --git a/spec/policies/service_provider_mfa_policy_spec.rb b/spec/policies/service_provider_mfa_policy_spec.rb index 86546d0f1c2..d9de0f19f66 100644 --- a/spec/policies/service_provider_mfa_policy_spec.rb +++ b/spec/policies/service_provider_mfa_policy_spec.rb @@ -15,6 +15,7 @@ identity_proofing?: false, biometric_comparison?: false, ialmax?: false, + enhanced_ipp?: false, ) end let(:auth_methods_session) { AuthMethodsSession.new(user_session: {}) } diff --git a/spec/requests/rack_attack_spec.rb b/spec/requests/rack_attack_spec.rb index 83800fa8419..491f1c25f89 100644 --- a/spec/requests/rack_attack_spec.rb +++ b/spec/requests/rack_attack_spec.rb @@ -66,7 +66,7 @@ end context 'when the request is for a pack' do - let(:pack_url) { '/packs/js/application.js' } + let(:pack_url) { '/packs/application.js' } let(:pack_path) { Rails.public_path.join(pack_url.sub(/^\//, '')) } before do diff --git a/spec/services/authn_context_resolver_spec.rb b/spec/services/authn_context_resolver_spec.rb index 7c5c8589b2b..d95ddcc46eb 100644 --- a/spec/services/authn_context_resolver_spec.rb +++ b/spec/services/authn_context_resolver_spec.rb @@ -1,11 +1,14 @@ require 'rails_helper' RSpec.describe AuthnContextResolver do + let(:user) { build(:user) } + context 'when the user uses a vtr param' do it 'parses the vtr param into requirements' do vtr = ['C2.Pb'] result = AuthnContextResolver.new( + user: user, service_provider: nil, vtr: vtr, acr_values: nil, @@ -18,6 +21,27 @@ expect(result.identity_proofing?).to eq(true) expect(result.biometric_comparison?).to eq(true) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) + end + + it 'parses the vtr param for enhanced ipp' do + vtr = ['Pe'] + + result = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + + expect(result.component_values.map(&:name).join('.')).to eq('C1.C2.P1.Pe') + expect(result.aal2?).to eq(true) + expect(result.phishing_resistant?).to eq(false) + expect(result.hspd12?).to eq(false) + expect(result.identity_proofing?).to eq(true) + expect(result.biometric_comparison?).to eq(false) + expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(true) end it 'ignores any acr_values params that are passed' do @@ -29,6 +53,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: nil, vtr: vtr, acr_values: acr_values, @@ -38,6 +63,91 @@ end end + context 'when the user uses a vtr param with multiple vectors' do + context 'a biometric proofing vector and non-biometric proofing vector is present' do + it 'returns a biometric requirement if the user can satisfy it' do + user = create(:user, :proofed) + user.active_profile.update!(idv_level: 'unsupervised_with_selfie') + vtr = ['C2.Pb', 'C2.P1'] + + result = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + + expect(result.expanded_component_values).to eq('C1.C2.P1.Pb') + expect(result.biometric_comparison?).to eq(true) + expect(result.identity_proofing?).to eq(true) + end + + it 'returns the non-biometric vector if the user has identity-proofed without biometric' do + user = create(:user, :proofed) + vtr = ['C2.Pb', 'C2.P1'] + + result = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + + expect(result.expanded_component_values).to eq('C1.C2.P1') + expect(result.biometric_comparison?).to eq(false) + expect(result.identity_proofing?).to eq(true) + end + + it 'returns the first vector if the user has not proofed' do + user = create(:user) + vtr = ['C2.Pb', 'C2.P1'] + + result = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + + expect(result.expanded_component_values).to eq('C1.C2.P1.Pb') + expect(result.biometric_comparison?).to eq(true) + expect(result.identity_proofing?).to eq(true) + end + end + + context 'a non-biometric identity proofing vector is present' do + it 'returns the identity-proofing requirement if the user can satisfy it' do + user = create(:user, :proofed) + vtr = ['C2.P1', 'C2'] + + result = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + + expect(result.expanded_component_values).to eq('C1.C2.P1') + expect(result.identity_proofing?).to eq(true) + end + + it 'returns the no-proofing vector if the user cannot satisfy the ID-proofing requirement' do + user = create(:user) + vtr = ['C2.P1', 'C2'] + + result = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + + expect(result.expanded_component_values).to eq('C1.C2') + expect(result.identity_proofing?).to eq(false) + end + end + end + context 'when users uses an acr_values param' do context 'no service provider' do it 'parses an ACR value into requirements' do @@ -47,6 +157,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: nil, vtr: nil, acr_values: acr_values, @@ -59,6 +170,7 @@ expect(result.identity_proofing?).to eq(false) expect(result.biometric_comparison?).to eq(false) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) end it 'properly parses an ACR value without an AAL ACR' do @@ -67,6 +179,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: nil, vtr: nil, acr_values: acr_values, @@ -79,6 +192,7 @@ expect(result.identity_proofing?).to eq(false) expect(result.biometric_comparison?).to eq(false) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) end it 'properly parses an ACR value without an IAL ACR' do @@ -87,6 +201,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: nil, vtr: nil, acr_values: acr_values, @@ -99,6 +214,7 @@ expect(result.identity_proofing?).to eq(false) expect(result.biometric_comparison?).to eq(false) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) end end @@ -112,6 +228,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: nil, acr_values: acr_values, @@ -128,6 +245,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: nil, acr_values: acr_values, @@ -145,6 +263,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: nil, acr_values: acr_values, @@ -162,6 +281,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: nil, acr_values: acr_values, @@ -181,6 +301,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: nil, acr_values: acr_values, @@ -198,6 +319,7 @@ ].join(' ') result = AuthnContextResolver.new( + user: user, service_provider: service_provider, vtr: nil, acr_values: acr_values, diff --git a/spec/services/backup_code_generator_spec.rb b/spec/services/backup_code_generator_spec.rb index 0ab443c2f26..9d980f5a124 100644 --- a/spec/services/backup_code_generator_spec.rb +++ b/spec/services/backup_code_generator_spec.rb @@ -6,7 +6,7 @@ subject(:generator) { BackupCodeGenerator.new(user) } it 'should generate backup codes and be able to verify them' do - codes = generator.create + codes = generator.delete_and_regenerate codes.each do |code| expect(generator.verify(code)).to eq(true) @@ -17,7 +17,7 @@ expect(Base32::Crockford).to receive(:encode). and_call_original.at_least(BackupCodeGenerator::NUMBER_OF_CODES).times - codes = generator.create + codes = generator.delete_and_regenerate codes.each do |code| expect(code).to match(/\A[a-z0-9]{12}\Z/i) @@ -25,7 +25,7 @@ end it 'should reject invalid codes' do - generator.generate + generator.delete_and_regenerate success = generator.verify 'This is a string which will never result from code generation' expect(success).to eq false @@ -37,7 +37,7 @@ end it 'creates codes with the same salt for that batch' do - generator.create + generator.delete_and_regenerate salts = user.backup_code_configurations.map(&:code_salt).uniq expect(salts.size).to eq(1) @@ -52,7 +52,7 @@ user1 = create(:user) user2 = create(:user) - [user1, user2].each { |user| BackupCodeGenerator.new(user).create } + [user1, user2].each { |user| BackupCodeGenerator.new(user).delete_and_regenerate } user1_salt = user1.backup_code_configurations.map(&:code_salt).uniq.first user2_salt = user2.backup_code_configurations.map(&:code_salt).uniq.first diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index eee30ed3cb1..6afb19420a8 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -67,6 +67,20 @@ ].to_set, ) end + + it 'includes requested_attributes' do + result = subject.proof(state_id_data) + expect(result.requested_attributes).to eq( + { + dob: 1, + state_id_number: 1, + state_id_type: 1, + last_name: 1, + first_name: 1, + address: 1, + }, + ) + end end context 'when verification is unsuccessful' do @@ -98,6 +112,20 @@ ].to_set, ) end + + it 'includes requested_attributes' do + result = subject.proof(state_id_data) + expect(result.requested_attributes).to eq( + { + dob: 1, + state_id_number: 1, + state_id_type: 1, + last_name: 1, + first_name: 1, + address: 1, + }, + ) + end end context 'when verification attributes are missing' do @@ -128,6 +156,43 @@ ].to_set, ) end + + it 'includes requested_attributes' do + result = subject.proof(state_id_data) + expect(result.requested_attributes).to eq( + { + state_id_number: 1, + state_id_type: 1, + last_name: 1, + first_name: 1, + address: 1, + }, + ) + end + end + + context 'when issue / expiration present' do + let(:state_id_data) do + { + state_id_number: '1234567890', + state_id_jurisdiction: 'VA', + state_id_type: 'drivers_license', + state_id_issued: '2023-04-05', + state_id_expiration: '2030-01-02', + } + end + + it 'includes them' do + expect(Proofing::Aamva::Request::VerificationRequest).to receive(:new).with( + hash_including( + applicant: satisfy do |a| + expect(a.state_id_data.state_id_issued).to eql('2023-04-05') + expect(a.state_id_data.state_id_expiration).to eql('2030-01-02') + end, + ), + ) + subject.proof(state_id_data) + end end context 'when AAMVA throws an exception' do diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb index 2f379d5a911..14de5da2a5d 100644 --- a/spec/services/proofing/aamva/request/verification_request_spec.rb +++ b/spec/services/proofing/aamva/request/verification_request_spec.rb @@ -47,7 +47,10 @@ applicant.address2 = 'Apt 1' document = REXML::Document.new(subject.body) - address_node = REXML::XPath.first(document, '//ns:verifyDriverLicenseDataRequest/ns1:Address') + address_node = REXML::XPath.first( + document, + '//dldv:verifyDriverLicenseDataRequest/aa:Address', + ) address_node_element_names = address_node.elements.map(&:name) address_node_element_values = address_node.elements.map(&:text) @@ -71,6 +74,20 @@ ], ) end + + it 'includes issue date if present' do + applicant.state_id_data.state_id_issued = '2024-05-06' + expect(subject.body).to include( + '2024-05-06', + ) + end + + it 'includes expiration date if present' do + applicant.state_id_data.state_id_expiration = '2030-01-02' + expect(subject.body).to include( + '2030-01-02', + ) + end end describe '#headers' do @@ -142,7 +159,7 @@ let(:state_id_jurisdiction) { 'SC' } let(:rendered_state_id_number) do body = REXML::Document.new(subject.body) - REXML::XPath.first(body, '//ns2:IdentificationID')&.text + REXML::XPath.first(body, '//nc:IdentificationID')&.text end context 'id is greater than 8 digits' do diff --git a/spec/services/proofing/aamva/response/verification_response_spec.rb b/spec/services/proofing/aamva/response/verification_response_spec.rb index 47d57cba506..2bd0287eb63 100644 --- a/spec/services/proofing/aamva/response/verification_response_spec.rb +++ b/spec/services/proofing/aamva/response/verification_response_spec.rb @@ -13,6 +13,8 @@ end let(:verification_results) do { + state_id_expiration: nil, + state_id_issued: nil, state_id_number: true, state_id_type: true, dob: true, diff --git a/spec/services/proofing/aamva/verification_client_spec.rb b/spec/services/proofing/aamva/verification_client_spec.rb index 9638d4d7fd9..dc0f0b9d3c5 100644 --- a/spec/services/proofing/aamva/verification_client_spec.rb +++ b/spec/services/proofing/aamva/verification_client_spec.rb @@ -25,7 +25,7 @@ verification_stub = stub_request(:post, AamvaFixtures.example_config.verification_url). to_return(body: AamvaFixtures.verification_response, status: 200). with do |request| - xml_text_at_path(request.body, '//ns:token').gsub(/\s/, '') == 'ThisIsTheToken' + xml_text_at_path(request.body, '//dldv:token').gsub(/\s/, '') == 'ThisIsTheToken' end verification_client.send_verification_request( diff --git a/spec/services/proofing/mock/state_id_mock_client_spec.rb b/spec/services/proofing/mock/state_id_mock_client_spec.rb index c99819a8d43..a342cff43c9 100644 --- a/spec/services/proofing/mock/state_id_mock_client_spec.rb +++ b/spec/services/proofing/mock/state_id_mock_client_spec.rb @@ -19,6 +19,7 @@ errors: {}, exception: nil, mva_exception: nil, + requested_attributes: {}, timed_out: false, transaction_id: transaction_id, vendor_name: 'StateIdMock', @@ -43,6 +44,7 @@ }, exception: nil, mva_exception: nil, + requested_attributes: {}, timed_out: false, transaction_id: transaction_id, vendor_name: 'StateIdMock', @@ -65,6 +67,7 @@ errors: {}, exception: an_instance_of(Proofing::TimeoutError), mva_exception: true, + requested_attributes: {}, timed_out: true, transaction_id: transaction_id, vendor_name: 'StateIdMock', diff --git a/spec/services/proofing/state_id_result_spec.rb b/spec/services/proofing/state_id_result_spec.rb new file mode 100644 index 00000000000..eb40e013c3f --- /dev/null +++ b/spec/services/proofing/state_id_result_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe Proofing::StateIdResult do + let(:success) { true } + let(:errors) { {} } + let(:exception) { nil } + let(:vendor_name) { 'aamva' } + let(:transaction_id) { 'ABCD1234' } + let(:requested_attributes) { { dob: 1, first_name: 1 } } + let(:verified_attributes) { [:dob, :first_name] } + + subject do + described_class.new( + success:, + errors:, + exception:, + vendor_name:, + transaction_id:, + requested_attributes:, + verified_attributes:, + ) + end + + describe '#to_h' do + it 'includes the right attributes' do + expect(subject.to_h).to eql( + { + success: true, + errors: {}, + exception: nil, + mva_exception: nil, + vendor_name: 'aamva', + timed_out: false, + transaction_id: 'ABCD1234', + requested_attributes: { dob: 1, first_name: 1 }, + verified_attributes: [:dob, :first_name], + }, + ) + end + end +end diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index e4b63023f75..e1800ffb5de 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -302,6 +302,24 @@ end end + context 'multiple VTR for identity proofing with unauthorized SP for identity proofing' do + let(:authn_context) { ['C1', 'C1.P1'] } + before { sp.update!(ial: 1) } + + it 'returns FormResponse with success false' do + errors = { + authn_context: [t('errors.messages.unauthorized_authn_context')], + } + + expect(response.to_h).to include( + success: false, + errors: errors, + error_details: hash_including(*errors.keys), + **extra, + ) + end + end + context 'valid VTR but VTR is disallowed by config' do let(:use_vot_in_sp_requests) { false } let(:authn_context) { ['C1'] } @@ -337,4 +355,38 @@ end end end + + describe '#biometric_comparison_requested?' do + let(:sp) { ServiceProvider.find_by(issuer: 'http://localhost:3000') } + let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT } + let(:authn_context) { [] } + + subject(:validator) do + validator = SamlRequestValidator.new + validator.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + validator + end + + context 'biometric comparison was requested' do + let(:authn_context) { ['C1.C2.P1.Pb'] } + + it { expect(subject.biometric_comparison_requested?).to eq(true) } + end + + context 'biometric comparison was not requested' do + let(:authn_context) { ['C1.C2.P1'] } + + it { expect(subject.biometric_comparison_requested?).to eq(false) } + end + + context 'biometric comparison requested with multiple vectors of trust' do + let(:authn_context) { ['C1.C2.P1', 'C1.C2.P1.Pb'] } + + it { expect(subject.biometric_comparison_requested?).to eq(true) } + end + end end diff --git a/spec/services/user_alerts/alert_user_about_new_device_spec.rb b/spec/services/user_alerts/alert_user_about_new_device_spec.rb index 321619d2e9a..c69aa11899a 100644 --- a/spec/services/user_alerts/alert_user_about_new_device_spec.rb +++ b/spec/services/user_alerts/alert_user_about_new_device_spec.rb @@ -14,11 +14,10 @@ ).and_return(true) end - it 'sets the user sign_in_new_device_at value to time of the given event' do + it 'does not send any emails' do described_class.call(event:, device:, disavowal_token:) - expect(user.sign_in_new_device_at.change(usec: 0)).to be_present. - and eq(event.created_at.change(usec: 0)) + expect_delivered_email_count(0) end end @@ -51,6 +50,34 @@ end end + describe '.schedule_alert' do + subject(:result) { described_class.schedule_alert(event:) } + + context 'aggregated new device alerts enabled' do + before do + allow(IdentityConfig.store).to receive(:feature_new_device_alert_aggregation_enabled). + and_return(true) + end + + it 'sets the user sign_in_new_device_at value to time of the given event' do + expect { result }.to change { user.reload.sign_in_new_device_at&.change(usec: 0) }. + from(nil). + to(event.created_at.change(usec: 0)) + end + end + + context 'aggregated new device alerts disabled' do + before do + allow(IdentityConfig.store).to receive(:feature_new_device_alert_aggregation_enabled). + and_return(false) + end + + it 'does not set sign_in_new_device_at value' do + expect { result }.not_to change { user.reload.sign_in_new_device_at&.change(usec: 0) } + end + end + end + describe '.send_alert' do let(:sign_in_new_device_at) { 3.minutes.ago } let(:user) { create(:user, :fully_registered, sign_in_new_device_at:) } diff --git a/spec/services/vot/parser_spec.rb b/spec/services/vot/parser_spec.rb index 40af3ce6986..a59813bc188 100644 --- a/spec/services/vot/parser_spec.rb +++ b/spec/services/vot/parser_spec.rb @@ -22,6 +22,7 @@ expect(result.identity_proofing?).to eq(false) expect(result.biometric_comparison?).to eq(false) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) end end @@ -38,6 +39,22 @@ expect(result.identity_proofing?).to eq(true) expect(result.biometric_comparison?).to eq(true) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) + end + + it 'adds the Enhanced In Person Proofing components' do + vector_of_trust = 'Pe' + + result = Vot::Parser.new(vector_of_trust:).parse + + expect(result.component_values.map(&:name).join('.')).to eq('C1.C2.P1.Pe') + expect(result.aal2?).to eq(true) + expect(result.phishing_resistant?).to eq(false) + expect(result.hspd12?).to eq(false) + expect(result.identity_proofing?).to eq(true) + expect(result.biometric_comparison?).to eq(false) + expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(true) end end @@ -77,6 +94,7 @@ expect(result.identity_proofing?).to eq(true) expect(result.biometric_comparison?).to eq(false) expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) end end end diff --git a/spec/support/fake_saml_request.rb b/spec/support/fake_saml_request.rb index 02e6b633417..a9837156047 100644 --- a/spec/support/fake_saml_request.rb +++ b/spec/support/fake_saml_request.rb @@ -38,7 +38,7 @@ def requested_aal_authn_context Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF end - def requested_vtr_authn_context + def requested_vtr_authn_contexts nil end diff --git a/spec/support/flow_policy_helper.rb b/spec/support/flow_policy_helper.rb index 6e31e100bdf..7f26c330cee 100644 --- a/spec/support/flow_policy_helper.rb +++ b/spec/support/flow_policy_helper.rb @@ -15,6 +15,7 @@ def stub_step(key:, idv_session:) idv_session.idv_consent_given = true when :how_to_verify idv_session.skip_doc_auth = false + idv_session.skip_doc_auth_from_how_to_verify = false when :hybrid_handoff idv_session.flow_path = 'standard' when :link_sent diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index 478a216959e..a5b9804e42d 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -16,6 +16,7 @@ let(:acuant_version) { '1.3.3.7' } let(:skip_doc_auth) { false } + let(:skip_doc_auth_from_how_to_verify) { false } let(:skip_doc_auth_from_handoff) { false } let(:opted_in_to_in_person_proofing) { false } @@ -48,7 +49,7 @@ acuant_version: acuant_version, doc_auth_selfie_capture: selfie_capture_enabled, skip_doc_auth: skip_doc_auth, - skip_doc_auth_from_how_to_verify: false, + skip_doc_auth_from_how_to_verify: skip_doc_auth_from_how_to_verify, skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, opted_in_to_in_person_proofing: opted_in_to_in_person_proofing, } diff --git a/spec/views/users/backup_code_setup/index.html.erb_spec.rb b/spec/views/users/backup_code_setup/create.html.erb_spec.rb similarity index 88% rename from spec/views/users/backup_code_setup/index.html.erb_spec.rb rename to spec/views/users/backup_code_setup/create.html.erb_spec.rb index 8a3e91c2caf..3101dab75fa 100644 --- a/spec/views/users/backup_code_setup/index.html.erb_spec.rb +++ b/spec/views/users/backup_code_setup/create.html.erb_spec.rb @@ -1,12 +1,12 @@ require 'rails_helper' -RSpec.describe 'users/backup_code_setup/index.html.erb' do - let(:user) { build(:user, :fully_registered) } +RSpec.describe 'users/backup_code_setup/create.html.erb' do + let(:user) { create(:user, :fully_registered) } before do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(false) - @codes = BackupCodeGenerator.new(user).create + @codes = BackupCodeGenerator.new(user).delete_and_regenerate end it 'has a localized title' do @@ -62,7 +62,7 @@ before do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(true) - @codes = BackupCodeGenerator.new(user).create + @codes = BackupCodeGenerator.new(user).delete_and_regenerate end it 'shows a link to cancel backup code creation and choose another mfa option' do diff --git a/spec/views/users/backup_code_setup/edit.html.erb_spec.rb b/spec/views/users/backup_code_setup/edit.html.erb_spec.rb index dee4a74f66c..cd2c972ec50 100644 --- a/spec/views/users/backup_code_setup/edit.html.erb_spec.rb +++ b/spec/views/users/backup_code_setup/edit.html.erb_spec.rb @@ -3,14 +3,28 @@ RSpec.describe 'users/backup_code_setup/edit.html.erb' do subject(:rendered) { render } - it 'has a link to confirm and proceed to setup' do - expect(rendered).to have_link( - t('account.index.backup_code_confirm_regenerate'), - href: backup_code_setup_path, + it 'has a button to confirm and proceed to setup' do + expect(rendered).to have_css( + "form[method=post][action='#{backup_code_setup_path}']:not(:has([name=_method]))", + text: t('account.index.backup_code_confirm_regenerate'), ) end it 'has a link to cancel and return to account page' do expect(rendered).to have_link(t('links.cancel'), href: account_path) end + + context 'backup code confirm setup feature disabled' do + before do + allow(IdentityConfig.store).to receive(:backup_code_confirm_setup_screen_enabled). + and_return(false) + end + + it 'has a link to confirm and proceed to setup' do + expect(rendered).to have_link( + t('account.index.backup_code_confirm_regenerate'), + href: backup_code_setup_path, + ) + end + end end diff --git a/spec/views/users/backup_code_setup/new.html.erb_spec.rb b/spec/views/users/backup_code_setup/new.html.erb_spec.rb new file mode 100644 index 00000000000..f9a5cf9992c --- /dev/null +++ b/spec/views/users/backup_code_setup/new.html.erb_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +RSpec.describe 'users/backup_code_setup/new.html.erb' do + it 'has a localized title' do + expect(view).to receive(:title=).with( + t('two_factor_authentication.confirm_backup_code_setup_title'), + ) + + render + end + + it 'has a localized heading' do + render + + expect(rendered).to have_css( + 'h1', + text: t('two_factor_authentication.confirm_backup_code_setup_title'), + ) + end + + it 'has a button to continue' do + render + + expect(rendered).to have_css( + "form[method=post][action='#{backup_code_setup_path}']:not(:has([name=_method]))", + text: t('forms.buttons.continue'), + ) + end + + it 'has a link to cancel' do + render + + expect(rendered).to have_link(t('links.cancel'), href: account_path) + end + + context 'with account redirect path session value' do + let(:account_redirect_path) { account_two_factor_authentication_path } + + before do + session[:account_redirect_path] = account_redirect_path + end + + it 'has a link to cancel and return to account redirect path' do + render + + expect(rendered).to have_link(t('links.cancel'), href: account_redirect_path) + end + end +end diff --git a/webpack.config.js b/webpack.config.js index 66d13a9881a..fdec2e0ed53 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,9 +45,9 @@ module.exports = /** @type {import('webpack').Configuration} */ ({ return result; }, {}), output: { - filename: `js/[name]${hashSuffix}.js`, - chunkFilename: `js/[name].chunk${hashSuffix}.js`, - sourceMapFilename: `js/[name]${hashSuffix}.js.map`, + filename: `[name]${hashSuffix}.js`, + chunkFilename: `[name].chunk${hashSuffix}.js`, + sourceMapFilename: `[name]${hashSuffix}.js.map`, path: resolve(__dirname, 'public/packs'), publicPath: devServerPort && isLocalhost ? `http://localhost:${devServerPort}/packs/` : '/packs/',