diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e806394481e..b4a64718012 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ variables: GITLAB_CI: 'true' JUNIT_OUTPUT: 'true' ECR_REGISTRY: '${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com' - IDP_CI_SHA: 'sha256:cea459aea56802327075b873cc73a8859ecffa359a9311b359ea49b19b1ba934' + IDP_CI_SHA: 'sha256:f5bbf6917b20e559176962ddf499cf320a873b0099711ef4dae4fd7b5cb7f3eb' default: image: '${ECR_REGISTRY}/idp/ci@${IDP_CI_SHA}' diff --git a/.ruby-version b/.ruby-version index b0f2dcb32fc..944880fa15e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.4 +3.2.0 diff --git a/Gemfile b/Gemfile index c7c04f34e87..c26bceb89f1 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,7 @@ gem 'aws-sdk-ses', '~> 1.6' gem 'aws-sdk-sns' gem 'barby', '~> 0.6.8' gem 'base32-crockford' -gem 'bootsnap', '~> 1.9.0', require: false +gem 'bootsnap', '~> 1.0', require: false gem 'browser' gem 'connection_pool' gem 'cssbundling-rails' @@ -25,7 +25,7 @@ gem 'foundation_emails' gem 'good_job', '~> 3.0' gem 'hashie', '~> 4.1' gem 'http_accept_language' -gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v3.4.1' +gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v3.4.2' gem 'identity-logging', github: '18F/identity-logging', tag: 'v0.1.0' gem 'identity_validations', github: '18F/identity-validations', tag: 'v0.7.2' gem 'jsbundling-rails', '~> 1.0.0' @@ -57,7 +57,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.18.0-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.18.1-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'sprockets-rails' @@ -67,7 +67,7 @@ gem 'subprocess', require: false gem 'uglifier', '~> 4.2' gem 'valid_email', '>= 0.1.3' gem 'view_component', '~> 2.51.0' -gem 'webauthn', '~> 2.1' +gem 'webauthn', '~> 2.5.2' gem 'xmldsig', '~> 0.6' gem 'xmlenc', '~> 0.7', '>= 0.7.1' gem 'yard' @@ -82,6 +82,7 @@ group :development do gem 'derailed_benchmarks', '~> 1.8' gem 'guard-rspec', require: false gem 'irb' + gem 'letter_opener', '~> 1.8' gem 'octokit', '>= 4.25.0' gem 'rack-mini-profiler', '>= 1.1.3', require: false gem 'rails-erd', '>= 1.6.0' @@ -91,12 +92,11 @@ group :development, :test do gem 'aws-sdk-cloudwatchlogs', require: false gem 'brakeman', require: false gem 'bullet', '~> 7.0' - gem 'capybara-webmock', git: 'https://github.com/hashrocket/capybara-webmock.git', ref: '63d790a0' - gem 'data_uri', require: false + gem 'capybara-webmock', git: 'https://github.com/hashrocket/capybara-webmock.git', ref: 'd3f3b7c' gem 'erb_lint', '~> 0.3.0', require: false - gem 'i18n-tasks', '>= 0.9.31' + gem 'i18n-tasks', '~> 1.0' gem 'knapsack' - gem 'nokogiri', '~> 1.13.10' + gem 'nokogiri', '~> 1.14.0' gem 'parallel_tests' gem 'pg_query', require: false gem 'pry-byebug' diff --git a/Gemfile.lock b/Gemfile.lock index 634baa1ff78..76bdfc79478 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GIT remote: https://github.com/18F/identity-hostdata.git - revision: 25a7e98919b1eb0d61dbcce314807a412aff62ad - tag: v3.4.1 + revision: e33cbdb3a9c8826f6fc2b1f857fb713a4a233750 + tag: v3.4.2 specs: identity-hostdata (3.4.1) activesupport (>= 6.1, < 8) @@ -25,8 +25,8 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: 7f516c9e2c608ac92ee0c41daecfdb9208c7ec5a - tag: 0.18.0-18f + revision: d8e7deb7da3aa43bae0e5b0891c8de123d492484 + tag: 0.18.1-18f specs: saml_idp (0.18.0.pre.18f) activesupport @@ -34,12 +34,11 @@ GIT faraday nokogiri (>= 1.10.2) pkcs11 - uuid GIT remote: https://github.com/hashrocket/capybara-webmock.git - revision: 63d790a0b6c779b9700634bfc153e25ccdeb3688 - ref: 63d790a0 + revision: d3f3b7c8edbeca7b575e74b256ad22df80d2b420 + ref: d3f3b7c specs: capybara-webmock (0.6.0) capybara (>= 2.4, < 4) @@ -117,8 +116,8 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) ahoy_matey (3.3.0) activesupport (>= 5) device_detector @@ -128,17 +127,17 @@ GEM ast (2.4.2) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.543.0) + aws-partitions (1.684.0) aws-sdk-cloudwatchlogs (1.49.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-core (3.125.0) + aws-sdk-core (3.168.4) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.61.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) aws-sdk-pinpoint (1.62.0) aws-sdk-core (~> 3, >= 3.122.0) @@ -146,8 +145,8 @@ GEM aws-sdk-pinpointsmsvoice (1.29.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.110.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-s3 (1.117.2) + aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sdk-ses (1.44.0) @@ -156,7 +155,7 @@ GEM aws-sdk-sns (1.49.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) axe-core-api (4.3.2) dumb_delegator @@ -184,11 +183,11 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bindata (2.4.10) + bindata (2.4.14) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.9.4) - msgpack (~> 1.0) + bootsnap (1.15.0) + msgpack (~> 1.2) brakeman (5.4.0) browser (5.3.1) builder (3.2.4) @@ -199,7 +198,7 @@ GEM bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) - capybara (3.36.0) + capybara (3.38.0) addressable matrix mini_mime (>= 0.1.3) @@ -209,7 +208,6 @@ GEM regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) cbor (0.5.9.6) - childprocess (4.1.0) choice (0.2.0) chunky_png (1.4.0) coderay (1.1.3) @@ -217,17 +215,16 @@ GEM descendants_tracker (~> 0.0.1) concurrent-ruby (1.1.10) connection_pool (2.2.5) - cose (1.2.0) + cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) crack (0.4.5) rexml crass (1.0.6) - css_parser (1.11.0) + css_parser (1.14.0) addressable cssbundling-rails (1.0.0) railties (>= 6.0.0) - data_uri (0.1.0) debug_inspector (1.1.0) derailed_benchmarks (1.8.1) benchmark-ips (~> 2) @@ -306,7 +303,7 @@ GEM railties (>= 6.0.0) thor (>= 0.14.1) webrick (>= 1.3) - google-protobuf (3.21.9) + google-protobuf (3.21.12) guard (2.16.2) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) @@ -325,15 +322,16 @@ GEM hashie (4.1.0) heapy (0.2.0) thor - highline (2.0.3) + highline (2.1.0) htmlbeautifier (1.4.2) htmlentities (4.3.4) http_accept_language (2.1.1) i18n (1.12.0) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.37) + i18n-tasks (1.0.12) activesupport (>= 4.0.2) ast (>= 2.1.0) + better_html (>= 1.0, < 3.0) erubi highline (>= 2.0.0) i18n @@ -343,10 +341,9 @@ GEM terminal-table (>= 1.5.1) ice_nine (0.11.2) io-console (0.5.9) - ipaddr (1.2.4) irb (1.3.7) reline (>= 0.2.7) - jmespath (1.6.1) + jmespath (1.6.2) jsbundling-rails (1.0.0) railties (>= 6.0.0) json (2.6.3) @@ -356,6 +353,8 @@ GEM rake launchy (2.5.0) addressable (~> 2.7) + letter_opener (1.8.1) + launchy (>= 2.2, < 3) listen (3.5.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -382,8 +381,6 @@ GEM zeitwerk (~> 2.5) lru_redux (1.1.0) lumberjack (1.2.8) - macaddr (1.7.2) - systemu (~> 2.6.5) mail (2.7.1) mini_mime (>= 0.1.1) marcel (1.0.2) @@ -415,9 +412,9 @@ GEM net-protocol timeout net-ssh (6.1.0) - newrelic_rpm (8.12.0) + newrelic_rpm (8.15.0) nio4r (2.5.8) - nokogiri (1.13.10) + nokogiri (1.14.0) mini_portile2 (~> 2.8.0) racc (~> 1.4) notiffany (0.1.3) @@ -426,8 +423,7 @@ GEM octokit (5.1.0) faraday (>= 1, < 3) sawyer (~> 0.9) - openssl (2.2.1) - ipaddr + openssl (3.0.2) openssl-signature_algorithm (1.2.1) openssl (> 2.0, < 3.1) orm_adapter (0.5.0) @@ -449,19 +445,19 @@ GEM actionmailer (>= 3) premailer (~> 1.7, >= 1.7.9) profanity_filter (0.1.1) - pry (0.13.1) + pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.9.0) + pry-byebug (3.10.1) byebug (~> 11.0) - pry (~> 0.13.0) - pry-doc (1.2.0) + pry (>= 0.13, < 0.15) + pry-doc (1.4.0) pry (~> 0.11) yard (~> 0.9.11) pry-rails (0.3.9) pry (>= 0.10.4) psych (4.0.2) - public_suffix (4.0.6) + public_suffix (5.0.1) puma (5.6.4) nio4r (~> 2.0) raabro (1.4.0) @@ -474,7 +470,7 @@ GEM rack-headers_filter (0.0.1) rack-mini-profiler (2.3.3) rack (>= 1.2.0) - rack-proxy (0.7.2) + rack-proxy (0.7.4) rack rack-test (2.0.2) rack (>= 1.3) @@ -510,7 +506,7 @@ GEM ruby-graphviz (~> 1.2) rails-html-sanitizer (1.4.4) loofah (~> 2.19, >= 2.19.1) - rails-i18n (7.0.3) + rails-i18n (7.0.6) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) railties (7.0.4) @@ -527,12 +523,15 @@ GEM ffi (~> 1.0) redacted_struct (1.1.0) redcarpet (3.5.1) - redis (4.7.1) + redis (5.0.5) + redis-client (>= 0.9.0) + redis-client (0.12.0) + connection_pool redis-namespace (1.8.1) redis (>= 3.0.4) - redis-session-store (0.11.4) - actionpack (>= 3, < 8) - redis (>= 3, < 5) + redis-session-store (0.11.5) + actionpack (>= 6, < 8) + redis (>= 3, < 6) regexp_parser (2.6.1) reline (0.2.7) io-console (~> 0.5) @@ -549,18 +548,18 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.1) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.2) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) + rspec-support (~> 3.12.0) rspec-rails (6.0.1) actionpack (>= 6.1) activesupport (>= 6.1) @@ -571,7 +570,7 @@ GEM rspec-support (~> 3.11) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.11.1) + rspec-support (3.12.0) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.42.0) @@ -613,10 +612,10 @@ GEM faraday (>= 0.17.3, < 3) scrypt (3.0.7) ffi-compiler (>= 1.0, < 2.0) - selenium-webdriver (4.1.0) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.7.1) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) shellany (0.0.1) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) @@ -645,16 +644,16 @@ GEM stringex (2.8.5) strong_migrations (0.8.0) activerecord (>= 5.2) - strscan (3.0.1) + strscan (3.0.5) subprocess (1.5.5) - systemu (2.6.5) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) thor (1.2.1) thread_safe (0.3.6) timeout (0.3.0) - tpm-key_attestation (0.10.0) + tpm-key_attestation (0.11.0) bindata (~> 2.4) + openssl (> 2.0, < 3.1) openssl-signature_algorithm (~> 1.0) tzinfo (2.0.5) concurrent-ruby (~> 1.0) @@ -663,10 +662,8 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8) - unicode-display_width (2.4.0) + unicode-display_width (2.4.2) uniform_notifier (1.16.0) - uuid (2.3.9) - macaddr (~> 1.0) valid_email (0.1.4) activemodel mail (>= 2.6.1) @@ -680,15 +677,15 @@ GEM descendants_tracker (~> 0.0, >= 0.0.3) warden (1.2.9) rack (>= 2.0.9) - webauthn (2.5.1) + webauthn (2.5.2) android_key_attestation (~> 0.3.0) awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) - openssl (~> 2.2) + openssl (>= 2.2, < 3.1) safety_net_attestation (~> 0.4.0) - tpm-key_attestation (~> 0.10.0) + tpm-key_attestation (~> 0.11.0) webdrivers (5.2.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) @@ -698,6 +695,7 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) webrick (1.7.0) + websocket (1.2.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -712,7 +710,8 @@ GEM nokogiri (~> 1.11) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.26) + yard (0.9.28) + webrick (~> 1.7.0) zeitwerk (2.6.6) zonebie (0.6.1) zxcvbn (0.1.7) @@ -733,7 +732,7 @@ DEPENDENCIES base32-crockford better_errors (>= 2.5.1) binding_of_caller - bootsnap (~> 1.9.0) + bootsnap (~> 1.0) brakeman browser bullet (~> 7.0) @@ -741,7 +740,6 @@ DEPENDENCIES capybara-webmock! connection_pool cssbundling-rails - data_uri derailed_benchmarks (~> 1.8) devise (~> 4.8) dotiw (>= 4.0.1) @@ -756,7 +754,7 @@ DEPENDENCIES guard-rspec hashie (~> 4.1) http_accept_language - i18n-tasks (>= 0.9.31) + i18n-tasks (~> 1.0) identity-hostdata! identity-logging! identity_validations! @@ -765,6 +763,7 @@ DEPENDENCIES jwe jwt knapsack + letter_opener (~> 1.8) lograge (>= 0.11.2) lookbook (~> 1.4.5) lru_redux @@ -772,7 +771,7 @@ DEPENDENCIES multiset net-sftp newrelic_rpm (~> 8.0) - nokogiri (~> 1.13.10) + nokogiri (~> 1.14.0) octokit (>= 4.25.0) parallel_tests pg @@ -826,7 +825,7 @@ DEPENDENCIES uglifier (~> 4.2) valid_email (>= 0.1.3) view_component (~> 2.51.0) - webauthn (~> 2.1) + webauthn (~> 2.5.2) webdrivers (~> 5.2.0) webmock xmldsig (~> 0.6) @@ -836,7 +835,7 @@ DEPENDENCIES zxcvbn (= 0.1.7) RUBY VERSION - ruby 3.0.4p208 + ruby 3.2.0p0 BUNDLED WITH 2.2.33 diff --git a/Procfile b/Procfile index 669cc3b65e7..5bcd9cc496a 100644 --- a/Procfile +++ b/Procfile @@ -1,5 +1,4 @@ web: WEBPACK_PORT=${WEBPACK_PORT:-3035} bundle exec rackup config.ru --port ${PORT:-3000} --host ${FOREMAN_HOST:-${HOST:-localhost}} worker: bundle exec good_job start -mailcatcher: mailcatcher -f $([ -n "$HTTPS" ] && echo "--http-ip=0.0.0.0") js: WEBPACK_PORT=${WEBPACK_PORT:-3035} yarn webpack $([ -n "$HTTPS" ] && echo "--watch" || echo "serve") css: yarn build:css --watch diff --git a/README.md b/README.md index 8b8e695bb04..289666e7763 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,25 @@ We recommend using [Homebrew](https://brew.sh/), [rbenv](https://github.com/rben #### Viewing email messages - In local development, the application does not deliver real email messages. Instead, we use a tool called [Mailcatcher](https://github.com/sj26/mailcatcher) to capture all messages. + In local development, the application does not deliver real email messages. Instead, we use a tool + called [letter_opener](https://github.com/ryanb/letter_opener) to display messages. - - To view email messages which would have been sent, visit http://localhost:1080/ while the application is running. - - To view email templates with placeholder values, visit http://localhost:3000/rails/mailers/ to see a list of template previews. +##### Disabling letter opener new window behavior + + Letter opener will open each outgoing email in a new browser window or tab. In cases where this + will be annoying the application also supports writing outgoing emails to a file. To write emails + to a file add the following config to the `development` group in `config/application.yml`: + + ``` + development: + development_mailer_deliver_method: file + ``` + + After restarting the app emails will be written to the `tmp/mails` folder. + +##### Email template previews + + To view email templates with placeholder values, visit http://localhost:3000/rails/mailers/ to see a list of template previews. #### Translations diff --git a/app/components/download_button_component.rb b/app/components/download_button_component.rb index 579575e89be..5b1d251ad2d 100644 --- a/app/components/download_button_component.rb +++ b/app/components/download_button_component.rb @@ -19,10 +19,6 @@ def initialize(file_data:, file_name:, **tag_options) @file_name = file_name end - def call - content_tag(:'lg-download-button', super) - end - def content super || t('components.download_button.label') end diff --git a/app/components/download_button_component.ts b/app/components/download_button_component.ts deleted file mode 100644 index 99726c1b32a..00000000000 --- a/app/components/download_button_component.ts +++ /dev/null @@ -1 +0,0 @@ -import '@18f/identity-download-button/download-button-element'; diff --git a/app/components/language_picker_component.rb b/app/components/language_picker_component.rb index 0ed01910029..0c13fbbc8ce 100644 --- a/app/components/language_picker_component.rb +++ b/app/components/language_picker_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LanguagePickerComponent < BaseComponent attr_reader :tag_options diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fe78dc08a37..92b70ab5052 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base include VerifyProfileConcern include LocaleHelper @@ -431,12 +433,16 @@ def sp_session session.fetch(:sp, {}) end + # Retrieves the current service provider session hash's logged request URL, if present + # Conditionally sets the final_auth_request service provider session attribute + # when applicable (the original SP request is SAML) def sp_session_request_url_with_updated_params # Temporarily place SAML route update behind a feature flag if IdentityConfig.store.saml_internal_post return unless sp_session[:request_url].present? request_url = URI(sp_session[:request_url]) url = if request_url.path.match?('saml') + sp_session[:final_auth_request] = true complete_saml_url else # Login.gov redirects to the orginal request_url after a user authenticates diff --git a/app/controllers/concerns/idv/threat_metrix_concern.rb b/app/controllers/concerns/idv/threat_metrix_concern.rb index 7a264dcb04e..22de6ca98cd 100644 --- a/app/controllers/concerns/idv/threat_metrix_concern.rb +++ b/app/controllers/concerns/idv/threat_metrix_concern.rb @@ -6,7 +6,7 @@ module ThreatMetrixConcern THREAT_METRIX_WILDCARD_DOMAIN = '*.online-metrix.net' def override_csp_for_threat_metrix - return unless IdentityConfig.store.proofing_device_profiling_collecting_enabled + return unless FeatureManagement.proofing_device_profiling_collecting_enabled? return if params[:step] != 'ssn' diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 5acf7eb9b33..d7df3b73e8c 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -20,7 +20,12 @@ module SamlIdpAuthConcern def sign_out_if_forceauthn_is_true_and_user_is_signed_in return unless user_signed_in? && saml_request.force_authn? - sign_out unless sp_session[:request_url] == request.original_url + if IdentityConfig.store.saml_internal_post + sign_out unless sp_session[:final_auth_request] + sp_session[:final_auth_request] = false + else + sign_out unless sp_session[:request_url] == request.original_url + end end def check_sp_active diff --git a/app/controllers/idv/gpo_verify_controller.rb b/app/controllers/idv/gpo_verify_controller.rb index 6616c44f940..e42eafd9431 100644 --- a/app/controllers/idv/gpo_verify_controller.rb +++ b/app/controllers/idv/gpo_verify_controller.rb @@ -101,7 +101,7 @@ def confirm_verification_needed end def threatmetrix_enabled? - IdentityConfig.store.lexisnexis_threatmetrix_required_to_verify + FeatureManagement.proofing_device_profiling_decisioning_enabled? end end end diff --git a/app/controllers/idv/otp_delivery_method_controller.rb b/app/controllers/idv/otp_delivery_method_controller.rb deleted file mode 100644 index c111c5fca8e..00000000000 --- a/app/controllers/idv/otp_delivery_method_controller.rb +++ /dev/null @@ -1,123 +0,0 @@ -module Idv - class OtpDeliveryMethodController < ApplicationController - include IdvSession - include StepIndicatorConcern - include PhoneOtpRateLimitable - include PhoneOtpSendable - - # confirm_two_factor_authenticated before action is in PhoneOtpRateLimitable - before_action :confirm_phone_step_complete - before_action :confirm_step_needed - before_action :set_idv_phone - - def new - analytics.idv_phone_otp_delivery_selection_visit - render :new, locals: view_locals - end - - def create - result = otp_delivery_selection_form.submit(otp_delivery_selection_params) - analytics.idv_phone_otp_delivery_selection_submitted(**result.to_h) - - return render_new_with_error_message unless result.success? - send_phone_confirmation_otp_and_handle_result - end - - private - - attr_reader :idv_phone - - def view_locals - { - gpo_letter_available: gpo_letter_available, - phone_number_capabilities: phone_number_capabilities, - } - end - - def phone_number_capabilities - PhoneNumberCapabilities.new(idv_phone, phone_confirmed: user_phone?) - end - - def user_phone? - MfaContext.new(current_user).phone_configurations.any? { |config| config.phone == idv_phone } - end - - def confirm_phone_step_complete - redirect_to idv_phone_url if idv_session.vendor_phone_confirmation != true - end - - def confirm_step_needed - redirect_to idv_review_url if idv_session.address_verification_mechanism != 'phone' || - idv_session.user_phone_confirmation == true - end - - def set_idv_phone - @idv_phone = idv_session.user_phone_confirmation_session.phone - end - - def otp_delivery_selection_params - params.permit(:otp_delivery_preference) - end - - def render_new_with_error_message - flash[:error] = t('idv.errors.unsupported_otp_delivery_method') - render :new, locals: view_locals - end - - def send_phone_confirmation_otp_and_handle_result - save_delivery_preference - result = send_phone_confirmation_otp - analytics.idv_phone_confirmation_otp_sent( - **result.to_h.merge(adapter: Telephony.config.adapter), - ) - - irs_attempts_api_tracker.idv_phone_otp_sent( - phone_number: @idv_phone, - success: result.success?, - otp_delivery_method: params[:otp_delivery_preference], - failure_reason: result.success? ? {} : otp_sent_tracker_error(result), - ) - if result.success? - redirect_to idv_otp_verification_url - else - handle_send_phone_confirmation_otp_failure(result) - end - end - - def handle_send_phone_confirmation_otp_failure(result) - if send_phone_confirmation_otp_rate_limited? - handle_too_many_otp_sends - else - invalid_phone_number(result.extra[:telephony_response].error) - end - end - - def save_delivery_preference - original_session = idv_session.user_phone_confirmation_session - idv_session.user_phone_confirmation_session = Idv::PhoneConfirmationSession.new( - code: original_session.code, - phone: original_session.phone, - sent_at: original_session.sent_at, - delivery_method: @otp_delivery_selection_form.otp_delivery_preference.to_sym, - ) - end - - def otp_sent_tracker_error(result) - if send_phone_confirmation_otp_rate_limited? - { rate_limited: true } - else - { telephony_error: result.extra[:telephony_response]&.error&.friendly_message } - end - end - - def otp_delivery_selection_form - @otp_delivery_selection_form ||= Idv::OtpDeliveryMethodForm.new - end - - def gpo_letter_available - return @gpo_letter_available if defined?(@gpo_letter_available) - @gpo_letter_available ||= FeatureManagement.enable_gpo_verification? && - !Idv::GpoMail.new(current_user).mail_spammed? - end - end -end diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index 580cd0ba3ee..0e251cbc5a8 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -88,11 +88,8 @@ def pending_profile? end def blocked_by_device_profiling? - return false unless IdentityConfig.store.lexisnexis_threatmetrix_required_to_verify - proofing_component = ProofingComponent.find_by(user: current_user) - # pass users who are inbetween feature flag being enabled and have not had a check run. - return false if proofing_component.threatmetrix_review_status.nil? - proofing_component.threatmetrix_review_status != 'pass' + !idv_session.profile.active && + idv_session.profile.deactivation_reason == 'threatmetrix_review_pending' end end end diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index 77ca76c6630..6c29a9d7c22 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -61,8 +61,6 @@ def redirect_to_next_step if phone_confirmation_required? if VendorStatus.new.all_phone_vendor_outage? redirect_to vendor_outage_path(from: :idv_phone) - elsif step.otp_delivery_preference_missing? - redirect_to idv_otp_delivery_method_url else send_phone_confirmation_otp_and_handle_result end diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index c7650735abb..665d8e40934 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -1,6 +1,5 @@ require 'saml_idp_constants' require 'saml_idp' -require 'uuid' class SamlIdpController < ApplicationController include SamlIdp::Controller diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index 7b0116743b3..93e78461c6b 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -13,7 +13,7 @@ def show end def confirm - result = form.submit(request.protocol, params) + result = form.submit analytics.track_mfa_submit_event( result.to_h.merge(analytics_properties), ) @@ -121,7 +121,16 @@ def analytics_properties end def form - @form ||= WebauthnVerificationForm.new(current_user, user_session) + @form ||= WebauthnVerificationForm.new( + user: current_user, + challenge: user_session[:webauthn_challenge], + protocol: request.protocol, + authenticator_data: params[:authenticator_data], + client_data_json: params[:client_data_json], + signature: params[:signature], + credential_id: params[:credential_id], + webauthn_error: params[:webauthn_error], + ) end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 2fbe9f03162..58a8b12de18 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Users class SessionsController < Devise::SessionsController include ::ActionView::Helpers::DateHelper @@ -13,6 +15,7 @@ class SessionsController < Devise::SessionsController before_action :check_user_needs_redirect, only: [:new] before_action :apply_secure_headers_override, only: [:new, :create] before_action :clear_session_bad_password_count_if_window_expired, only: [:create] + before_action :update_devise_params_sanitizer, only: [:new] def new analytics.sign_in_page_visit( @@ -236,6 +239,10 @@ def override_csp_for_google_analytics policy.connect_src(*policy.connect_src, 'www.google-analytics.com') request.content_security_policy = policy end + + def update_devise_params_sanitizer + devise_parameter_sanitizer.permit(:sign_in, except: [:email, :password]) + end end def unsafe_redirect_error(_exception) diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb index 70f080de79a..8b33bb18dec 100644 --- a/app/forms/gpo_verify_form.rb +++ b/app/forms/gpo_verify_form.rb @@ -81,7 +81,7 @@ def pending_in_person_enrollment? end def threatmetrix_enabled? - IdentityConfig.store.lexisnexis_threatmetrix_required_to_verify + FeatureManagement.proofing_device_profiling_decisioning_enabled? end def threatmetrix_check_failed? diff --git a/app/forms/idv/document_capture_session_form.rb b/app/forms/idv/document_capture_session_form.rb index ecdc06aa59f..bc9d40d9dd1 100644 --- a/app/forms/idv/document_capture_session_form.rb +++ b/app/forms/idv/document_capture_session_form.rb @@ -25,7 +25,6 @@ def extra_analytics_attributes for_user_id: document_capture_session&.user_id, user_id: 'anonymous-uuid', event: 'Document capture session validation', - ial2_strict: document_capture_session&.ial2_strict?, sp_issuer: document_capture_session&.issuer, } end diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb index 70108207bae..ef29fb47898 100644 --- a/app/forms/idv/phone_form.rb +++ b/app/forms/idv/phone_form.rb @@ -90,7 +90,7 @@ def valid_phone_for_allowed_countries?(phone) def phone_info return @phone_info if defined?(@phone_info) - if phone.blank? || !IdentityConfig.store.voip_check + if phone.blank? || !IdentityConfig.store.phone_service_check @phone_info = nil else @phone_info = Telephony.phone_info(phone) diff --git a/app/forms/new_phone_form.rb b/app/forms/new_phone_form.rb index 75ec6a21b2f..3b60bc1021e 100644 --- a/app/forms/new_phone_form.rb +++ b/app/forms/new_phone_form.rb @@ -48,7 +48,7 @@ def delivery_preference_voice? def phone_info return @phone_info if defined?(@phone_info) - if phone.blank? || !IdentityConfig.store.voip_check + if phone.blank? || !IdentityConfig.store.phone_service_check @phone_info = nil else @phone_info = Telephony.phone_info(phone) @@ -91,7 +91,7 @@ def extra_analytics_attributes end def validate_not_voip - return if phone.blank? || !IdentityConfig.store.voip_check + return if phone.blank? || !IdentityConfig.store.phone_service_check return unless IdentityConfig.store.voip_block if phone_info.type == :voip && diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index 732d20264dd..40ce4cebb06 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class OpenidConnectAuthorizeForm include ActiveModel::Model include ActionView::Helpers::TranslationHelper diff --git a/app/forms/webauthn_verification_form.rb b/app/forms/webauthn_verification_form.rb index 3be58ff70e9..70a6a49b9e5 100644 --- a/app/forms/webauthn_verification_form.rb +++ b/app/forms/webauthn_verification_form.rb @@ -7,30 +7,45 @@ class WebauthnVerificationForm validates :authenticator_data, presence: true validates :client_data_json, presence: true validates :signature, presence: true + validates :webauthn_configuration, presence: true + validate :validate_assertion_response + validate :validate_webauthn_error - attr_accessor :webauthn_configuration - - def initialize(user, user_session) + def initialize( + protocol:, + user: nil, + challenge: nil, + authenticator_data: nil, + client_data_json: nil, + signature: nil, + credential_id: nil, + webauthn_error: nil + ) @user = user - @challenge = user_session[:webauthn_challenge] - @authenticator_data = nil - @client_data_json = nil - @signature = nil - @credential_id = nil - @webauthn_configuration = nil - @webauthn_errors = nil + @challenge = challenge + @protocol = protocol + @authenticator_data = authenticator_data + @client_data_json = client_data_json + @signature = signature + @credential_id = credential_id + @webauthn_error = webauthn_error end - def submit(protocol, params) - consume_parameters(params) - success = valid? && valid_assertion_response?(protocol) + def submit + success = valid? FormResponse.new( success: success, errors: errors, extra: extra_analytics_attributes, + serialize_error_details_only: true, ) end + def webauthn_configuration + return @webauthn_configuration if defined?(@webauthn_configuration) + @webauthn_configuration = user&.webauthn_configurations&.find_by(credential_id: credential_id) + end + # this gives us a hook to override the domain embedded in the attestation test object def self.domain_name IdentityConfig.store.domain_name @@ -38,46 +53,54 @@ def self.domain_name private - attr_reader :success, - :user, + attr_reader :user, :challenge, + :protocol, :authenticator_data, :client_data_json, - :signature + :signature, + :credential_id, + :webauthn_error - def consume_parameters(params) - @authenticator_data = params[:authenticator_data] - @client_data_json = params[:client_data_json] - @signature = params[:signature] - @credential_id = params[:credential_id] - @webauthn_errors = params[:errors] + def validate_assertion_response + return if webauthn_error.present? || webauthn_configuration.blank? || valid_assertion_response? + errors.add(:authenticator_data, :invalid_authenticator_data, type: :invalid_authenticator_data) end - def valid_assertion_response?(protocol) - return false if @webauthn_errors.present? - assertion_response = ::WebAuthn::AuthenticatorAssertionResponse.new( - authenticator_data: Base64.decode64(@authenticator_data), - client_data_json: Base64.decode64(@client_data_json), - signature: Base64.decode64(@signature), + def validate_webauthn_error + return if webauthn_error.blank? + errors.add(:webauthn_error, webauthn_error, type: :webauthn_error) + end + + def valid_assertion_response? + return false if authenticator_data.blank? || + client_data_json.blank? || + signature.blank? || + challenge.blank? + WebAuthn::AuthenticatorAssertionResponse.new( + authenticator_data: Base64.decode64(authenticator_data), + client_data_json: Base64.decode64(client_data_json), + signature: Base64.decode64(signature), + ).valid?( + challenge.pack('c*'), + original_origin, + public_key: Base64.decode64(public_key), + sign_count: 0, ) - original_origin = "#{protocol}#{self.class.domain_name}" - @webauthn_configuration = user.webauthn_configurations.find_by(credential_id: @credential_id) - return false unless @webauthn_configuration + rescue OpenSSL::PKey::PKeyError + false + end - public_key = @webauthn_configuration.credential_public_key + def original_origin + "#{protocol}#{self.class.domain_name}" + end - begin - assertion_response.valid?( - @challenge.pack('c*'), original_origin, - public_key: Base64.decode64(public_key), sign_count: 0 - ) - rescue OpenSSL::PKey::PKeyError - false - end + def public_key + webauthn_configuration&.credential_public_key end def extra_analytics_attributes - auth_method = if @webauthn_configuration&.platform_authenticator + auth_method = if webauthn_configuration&.platform_authenticator 'webauthn_platform' else 'webauthn' @@ -85,7 +108,7 @@ def extra_analytics_attributes { multi_factor_auth_method: auth_method, - webauthn_configuration_id: @webauthn_configuration&.id, + webauthn_configuration_id: webauthn_configuration&.id, } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 381782848e4..d65dca9f358 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationHelper def title(title) content_for(:title) { title } diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 6511948b341..8b2681c8fde 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -1,12 +1,12 @@ -module LinkHelper - EXTERNAL_LINK_CLASS = 'usa-link--external'.freeze +# frozen_string_literal: true +module LinkHelper def new_window_link_to(name = nil, options = nil, html_options = nil, &block) html_options, options, name = options, name, capture(&block) if block html_options ||= {} html_options[:target] = '_blank' - html_options[:class] = [*html_options[:class], EXTERNAL_LINK_CLASS] + html_options[:class] = [*html_options[:class], 'usa-link--external'] name = ERB::Util.unwrapped_html_escape(name).rstrip.html_safe # rubocop:disable Rails/OutputSafety name << content_tag('span', t('links.new_window'), class: 'usa-sr-only') diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index e6124b2b179..2a25870c853 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # rubocop:disable Rails/HelperInstanceVariable module ScriptHelper def javascript_include_tag_without_preload(...) diff --git a/app/javascript/packages/download-button/README.md b/app/javascript/packages/download-button/README.md deleted file mode 100644 index 2bea9377e3f..00000000000 --- a/app/javascript/packages/download-button/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# `@18f/identity-download-button` - -Custom element for a download button component. - -## Usage - -Importing the element will register the `` custom element: - -```ts -import '@18f/identity-download-button/download-button-element'; -``` - -The custom element will implement the copying behavior, but all markup must already exist. - -```html - - Download - -``` diff --git a/app/javascript/packages/download-button/download-button-element.spec.ts b/app/javascript/packages/download-button/download-button-element.spec.ts deleted file mode 100644 index 6696f00ac92..00000000000 --- a/app/javascript/packages/download-button/download-button-element.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import sinon from 'sinon'; -import { screen, fireEvent } from '@testing-library/dom'; -import { useDefineProperty } from '@18f/identity-test-helpers'; -import { dataURIToBlob } from './download-button-element'; - -describe('dataURIToBlob', () => { - it('converts a data URI to equivalent Blob', async () => { - const blob = dataURIToBlob('data:text/plain;charset=utf-8,hello%20world'); - - const result = await new Promise((resolve) => { - const reader = new FileReader(); - reader.addEventListener('load', () => resolve(reader.result as string)); - reader.readAsText(blob); - }); - - expect(result).to.equal('hello world'); - }); -}); - -describe('DownloadButtonElement', () => { - it('does not interfere with the click event', () => { - document.body.innerHTML = ` - - Download - - `; - - const link = screen.getByRole('link', { name: 'Download' }); - - const onClick = sinon.stub().callsFake((event: MouseEvent) => { - expect(event.defaultPrevented).to.be.false(); - - // Prevent default behavior, since JSDOM will otherwise throw an error on navigation. - event.preventDefault(); - }); - window.addEventListener('click', onClick); - fireEvent.click(link); - window.removeEventListener('click', onClick); - - expect(onClick).to.have.been.called(); - }); - - context('with legacy Microsoft proprietary download', () => { - const defineProperty = useDefineProperty(); - - it('prevents default and calls msSaveBlob', () => { - defineProperty(window.navigator, 'msSaveBlob', { value: sinon.stub(), configurable: true }); - - document.body.innerHTML = ` - - Download - - `; - - const link = screen.getByRole('link', { name: 'Download' }); - - const onClick = sinon.stub().callsFake((event: MouseEvent) => { - expect(event.defaultPrevented).to.be.true(); - - // Prevent default behavior, since JSDOM will otherwise throw an error on navigation. - event.preventDefault(); - }); - window.addEventListener('click', onClick); - fireEvent.click(link); - window.removeEventListener('click', onClick); - - expect(onClick).to.have.been.called(); - expect(window.navigator.msSaveBlob).to.have.been.called(); - }); - }); -}); diff --git a/app/javascript/packages/download-button/download-button-element.ts b/app/javascript/packages/download-button/download-button-element.ts deleted file mode 100644 index ac9a9c2ccd1..00000000000 --- a/app/javascript/packages/download-button/download-button-element.ts +++ /dev/null @@ -1,56 +0,0 @@ -declare global { - interface Navigator { - msSaveBlob?: (blob: Blob, filename: string) => void; - } -} - -/** - * Converts a data URI to a Blob. The given URI data must be URI-encoded and have a MIME type of - * `text/plain`. - * - * @param uri URI string to convert. - * - * @return Blob instance. - */ -export function dataURIToBlob(uri: string) { - const data = decodeURIComponent(uri.split(',')[1]); - const bytes = Uint8Array.from(data, (char) => char.charCodeAt(0)); - return new Blob([bytes], { type: 'text/plain' }); -} - -class DownloadButtonElement extends HTMLElement { - link: HTMLAnchorElement; - - connectedCallback() { - this.link = this.querySelector('a')!; - - if (window.navigator.msSaveBlob) { - this.link.addEventListener('click', this.triggerInternetExplorerDownload); - } - } - - /** - * Click handler to trigger download for legacy Microsoft proprietary download. - */ - triggerInternetExplorerDownload = (event: MouseEvent) => { - event.preventDefault(); - - const filename = this.link.getAttribute('download')!; - const uri = this.link.getAttribute('href')!; - const blob = new Blob([dataURIToBlob(uri)], { type: 'text/plain' }); - - window.navigator.msSaveBlob?.(blob, filename); - }; -} - -declare global { - interface HTMLElementTagNameMap { - 'lg-download-button': DownloadButtonElement; - } -} - -if (!customElements.get('lg-download-button')) { - customElements.define('lg-download-button', DownloadButtonElement); -} - -export default DownloadButtonElement; diff --git a/app/javascript/packages/download-button/package.json b/app/javascript/packages/download-button/package.json deleted file mode 100644 index b6efa66946e..00000000000 --- a/app/javascript/packages/download-button/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@18f/identity-download-button", - "version": "1.0.0", - "private": true -} 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 10838a751fa..fcfce8a5151 100644 --- a/app/javascript/packages/step-indicator/step-indicator-step.spec.tsx +++ b/app/javascript/packages/step-indicator/step-indicator-step.spec.tsx @@ -43,7 +43,7 @@ describe('StepIndicatorStep', () => { ); const title = getByText('Step'); - const status = getByText('step_indicator.status.current'); + const status = getByText('step_indicator.status.not_complete'); const step = title.closest('.step-indicator__step')!; expect(title).to.be.ok(); diff --git a/app/javascript/packages/step-indicator/step-indicator-step.tsx b/app/javascript/packages/step-indicator/step-indicator-step.tsx index 95d93add49b..0740cc87b34 100644 --- a/app/javascript/packages/step-indicator/step-indicator-step.tsx +++ b/app/javascript/packages/step-indicator/step-indicator-step.tsx @@ -3,7 +3,6 @@ import { t } from '@18f/identity-i18n'; export enum StepStatus { CURRENT, COMPLETE, - PENDING, INCOMPLETE, } @@ -20,7 +19,7 @@ export interface StepIndicatorStepProps { } function StepIndicatorStep({ title, status }: StepIndicatorStepProps) { - const { CURRENT, COMPLETE, PENDING } = StepStatus; + const { CURRENT, COMPLETE, INCOMPLETE } = StepStatus; const classes = [ 'step-indicator__step', @@ -35,8 +34,8 @@ function StepIndicatorStep({ title, status }: StepIndicatorStepProps) { case COMPLETE: statusText = t('step_indicator.status.complete'); break; - case PENDING: - statusText = t('step_indicator.status.pending'); + case INCOMPLETE: + statusText = t('step_indicator.status.not_complete'); break; default: statusText = t('step_indicator.status.current'); @@ -45,9 +44,7 @@ function StepIndicatorStep({ title, status }: StepIndicatorStepProps) { return (
  • {title} - - {statusText} - + {statusText}
  • ); } diff --git a/app/javascript/packages/time-element/index.spec.ts b/app/javascript/packages/time-element/index.spec.ts index edd6bb81f1e..6f6097ebb3a 100644 --- a/app/javascript/packages/time-element/index.spec.ts +++ b/app/javascript/packages/time-element/index.spec.ts @@ -48,20 +48,4 @@ describe('TimeElement', () => { expect(element.textContent).to.equal('April 21, 2020 at 14:03'); }); }); - - context('without formatToParts support', () => { - usePropertyValue(Intl.DateTimeFormat.prototype, 'formatToParts', undefined); - - it('sets text using Intl.DateTimeFormat#format as fallback', () => { - const date = new Date(2020, 3, 21, 14, 3, 24); - const element = createElement({ - format: '%{month} %{day}, %{year} at %{hour}:%{minute} %{day_period}', - timestamp: date.toISOString(), - }); - - const expected = element.formatter.format(date); - - expect(element.textContent).to.equal(expected); - }); - }); }); diff --git a/app/javascript/packages/time-element/index.ts b/app/javascript/packages/time-element/index.ts index 6f69310089c..9b94cff4fa6 100644 --- a/app/javascript/packages/time-element/index.ts +++ b/app/javascript/packages/time-element/index.ts @@ -41,19 +41,14 @@ export class TimeElement extends HTMLElement { setTime() { const { formatter } = this; - if (typeof formatter.formatToParts === 'function') { - const parts = Object.fromEntries( - formatter.formatToParts(this.date).map((part) => [part.type, part.value]), - ) as Partial>; - - this.textContent = replaceVariables( - this.#format, - mapKeys({ dayPeriod: '', ...parts }, snakeCase), - ); - } else { - // Degrade gracefully for environments where formatToParts is unsupported (Internet Explorer) - this.textContent = formatter.format(this.date); - } + const parts = Object.fromEntries( + formatter.formatToParts(this.date).map((part) => [part.type, part.value]), + ) as Partial>; + + this.textContent = replaceVariables( + this.#format, + mapKeys({ dayPeriod: '', ...parts }, snakeCase), + ); } } diff --git a/app/javascript/packs/webauthn-authenticate.ts b/app/javascript/packs/webauthn-authenticate.ts index 8eef0c60a02..86d5dcda7fa 100644 --- a/app/javascript/packs/webauthn-authenticate.ts +++ b/app/javascript/packs/webauthn-authenticate.ts @@ -37,7 +37,7 @@ function webauthn() { webauthnSuccessContainer.classList.remove('display-none'); }) .catch((error: Error) => { - (document.getElementById('errors') as HTMLInputElement).value = error.toString(); + (document.getElementById('webauthn_error') as HTMLInputElement).value = error.name; (document.getElementById('platform') as HTMLInputElement).value = String(webauthnPlatformRequested); (document.getElementById('webauthn_form') as HTMLFormElement).submit(); diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 4e81098f6c5..23cc3b6334b 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -106,7 +106,7 @@ def proof_lexisnexis_ddp_with_threatmetrix_if_needed( request_ip:, timer: ) - return unless IdentityConfig.store.lexisnexis_threatmetrix_enabled + return unless FeatureManagement.proofing_device_profiling_collecting_enabled? # The API call will fail without a session ID, so do not attempt to make # it to avoid leaking data when not required. diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb index 6675713d195..7d40550945c 100644 --- a/app/models/anonymous_user.rb +++ b/app/models/anonymous_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AnonymousUser def uuid 'anonymous-uuid' diff --git a/app/models/doc_auth_log.rb b/app/models/doc_auth_log.rb index b79991975bc..7ecd36b2821 100644 --- a/app/models/doc_auth_log.rb +++ b/app/models/doc_auth_log.rb @@ -6,4 +6,6 @@ class DocAuthLog < ApplicationRecord foreign_key: 'issuer', primary_key: 'issuer' # rubocop:enable Rails/InverseOf + + self.ignored_columns = ['selfie_view_at'] end diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 1889f7e0517..711603e4482 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -3,6 +3,8 @@ class DocumentCaptureSession < ApplicationRecord belongs_to :user + self.ignored_columns = ['ial2_strict'] + def load_result EncryptedRedisStructStorage.load(result_id, type: DocumentCaptureSessionResult) end diff --git a/app/services/browser_cache.rb b/app/services/browser_cache.rb index b13bcdf42c2..a72c9fd0b02 100644 --- a/app/services/browser_cache.rb +++ b/app/services/browser_cache.rb @@ -1,15 +1,17 @@ class BrowserCache @cache = LruRedux::Cache.new(1_000) + DEFAULT_BROWSER = Browser.new(nil) + USER_AGENT_SIZE = Browser.user_agent_size_limit - 1 # Detects browser attributes from User-Agent, truncated to 2047 bytes due # to: https://github.com/fnando/browser/blob/fa4f685482c315b8/lib/browser/browser.rb#L64-L65 # @param [String] user_agent # @return [Browser] def self.parse(user_agent) - return Browser.new(nil) if user_agent.nil? + return DEFAULT_BROWSER if user_agent.nil? @cache.getset(user_agent) do - Browser.new(user_agent.mb_chars.limit(Browser.user_agent_size_limit - 1).to_s) + Browser.new(user_agent.mb_chars.limit(USER_AGENT_SIZE).to_s) end end diff --git a/app/services/data_requests/lookup_shared_device_users.rb b/app/services/data_requests/lookup_shared_device_users.rb index 9a0e98bd8ec..e2089f7fd7e 100644 --- a/app/services/data_requests/lookup_shared_device_users.rb +++ b/app/services/data_requests/lookup_shared_device_users.rb @@ -11,7 +11,7 @@ module DataRequests class LookupSharedDeviceUsers attr_reader :initial_users, :depth - def initialize(initial_users, depth = 3) + def initialize(initial_users, depth = 1) @initial_users = initial_users @depth = depth @user_ids = initial_users.map(&:id).to_set diff --git a/app/services/encryption/aes_cipher.rb b/app/services/encryption/aes_cipher.rb index c020e9d723b..70c8cc321e6 100644 --- a/app/services/encryption/aes_cipher.rb +++ b/app/services/encryption/aes_cipher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Encryption class AesCipher include Encodable diff --git a/app/services/encryption/kms_client.rb b/app/services/encryption/kms_client.rb index 648d5d731fe..36830b43e9d 100644 --- a/app/services/encryption/kms_client.rb +++ b/app/services/encryption/kms_client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'base64' module Encryption @@ -9,6 +11,8 @@ class KmsClient KMS: 'KMSc', LOCAL_KEY: 'LOCc', }.freeze + KMS_KEY_REGEX = /\A#{KEY_TYPE[:KMS]}/ + LOCAL_KEY_REGEX = /\A#{KEY_TYPE[:LOCAL_KEY]}/ def encrypt(plaintext, encryption_context) KmsLogger.log(:encrypt, encryption_context) @@ -55,7 +59,7 @@ def encrypt_raw_kms(plaintext, encryption_context) end def decrypt_kms(ciphertext, encryption_context) - clipped_ciphertext = ciphertext.gsub(/\A#{KEY_TYPE[:KMS]}/, '') + clipped_ciphertext = ciphertext.gsub(KMS_KEY_REGEX, '') ciphertext_chunks = JSON.parse(clipped_ciphertext) ciphertext_chunks.map do |chunk| decrypt_raw_kms( @@ -82,7 +86,7 @@ def encrypt_local(plaintext, encryption_context) end def decrypt_local(ciphertext, encryption_context) - clipped_ciphertext = ciphertext.gsub(/\A#{KEY_TYPE[:LOCAL_KEY]}/, '') + clipped_ciphertext = ciphertext.gsub(LOCAL_KEY_REGEX, '') ciphertext_chunks = JSON.parse(clipped_ciphertext) ciphertext_chunks.map do |chunk| encryptor.decrypt( diff --git a/app/services/ial_context.rb b/app/services/ial_context.rb index ac1af1bb50d..ceeed8c283c 100644 --- a/app/services/ial_context.rb +++ b/app/services/ial_context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Wraps up logic for querying the IAL level of an authorization request class IalContext attr_reader :ial, :service_provider, :user, :authn_context_comparison diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index fb91d239b3b..c84d2c3fcfc 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -44,11 +44,6 @@ def async_state_done(async_state) ) end - def otp_delivery_preference_missing? - preference = idv_session.previous_phone_step_params[:otp_delivery_preference] - preference.nil? || preference.empty? - end - private attr_accessor :idv_session, :step_params, :idv_result diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 506b5a675cf..4e5f9a396ff 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -171,11 +171,20 @@ def in_person_enrollment? end def threatmetrix_failed_and_needs_review? - return unless IdentityConfig.store.lexisnexis_threatmetrix_required_to_verify - return unless IdentityConfig.store.lexisnexis_threatmetrix_enabled + failed_and_needs_review = true + ok_no_review_needed = false + + if !FeatureManagement.proofing_device_profiling_decisioning_enabled? + return ok_no_review_needed + end + component = ProofingComponent.find_by(user: @current_user) - return true unless component - !(component.threatmetrix && component.threatmetrix_review_status == 'pass') + + return ok_no_review_needed if !component.threatmetrix + + return ok_no_review_needed if component.threatmetrix_review_status == 'pass' + + return failed_and_needs_review end end end diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index 2b0fabdda26..d5ecb1e2488 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'set' class MarketingSite diff --git a/app/services/openid_connect_attribute_scoper.rb b/app/services/openid_connect_attribute_scoper.rb index 4f3951bac21..81a869b230d 100644 --- a/app/services/openid_connect_attribute_scoper.rb +++ b/app/services/openid_connect_attribute_scoper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class OpenidConnectAttributeScoper X509_SCOPES = %w[ x509 diff --git a/app/services/proofing/lexis_nexis/instant_verify/proofer.rb b/app/services/proofing/lexis_nexis/instant_verify/proofer.rb index b434c4dc81a..cc8111c8d3e 100644 --- a/app/services/proofing/lexis_nexis/instant_verify/proofer.rb +++ b/app/services/proofing/lexis_nexis/instant_verify/proofer.rb @@ -49,7 +49,7 @@ def parse_verification_errors(verification_response) # rubocop:disable Layout/LineLength def failed_result_can_pass_with_additional_verification?(verification_response) return false unless verification_response.verification_status == 'failed' - return false unless verification_response.verification_errors.keys.sort == [:"Execute Instant Verify", :base] + return false unless verification_response.verification_errors.keys.to_set == Set[:InstantVerify, :base] return false unless verification_response.verification_errors[:base].match?(/total\.scoring\.model\.verification\.fail/) return false unless attributes_requiring_additional_verification(verification_response).any? true @@ -58,14 +58,14 @@ def failed_result_can_pass_with_additional_verification?(verification_response) def attributes_requiring_additional_verification(verification_response) CheckToAttributeMapper.new( - verification_response.verification_errors[:"Execute Instant Verify"], + verification_response.verification_errors[:InstantVerify], ).map_failed_checks_to_attributes end def drivers_license_check_info(verification_response) instant_verify_product = verification_response.response_body['Products']&.first return unless instant_verify_product.present? - return unless instant_verify_product['ExecutedStepName'] == 'Execute Instant Verify' + return unless instant_verify_product['ProductType'] == 'InstantVerify' instant_verify_product['Items']&.find do |item| item['ItemName'] == 'DriversLicenseVerification' diff --git a/app/services/proofing/lexis_nexis/verification_error_parser.rb b/app/services/proofing/lexis_nexis/verification_error_parser.rb index 89d198bbdd1..3a5ce087e8d 100644 --- a/app/services/proofing/lexis_nexis/verification_error_parser.rb +++ b/app/services/proofing/lexis_nexis/verification_error_parser.rb @@ -52,7 +52,7 @@ def parse_product_error_messages # don't log PhoneFinder reflected PII product.delete('ParameterDetails') if product['ProductType'] == 'PhoneFinder' - key = product.fetch('ExecutedStepName').to_sym + key = product.fetch('ProductType').to_sym error_messages[key] = product end end diff --git a/app/services/secure_headers_allow_list.rb b/app/services/secure_headers_allow_list.rb index 05dd65a1358..44e030976a7 100644 --- a/app/services/secure_headers_allow_list.rb +++ b/app/services/secure_headers_allow_list.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SecureHeadersAllowList def self.csp_with_sp_redirect_uris(action_url_domain, sp_redirect_uris) ["'self'"] + reduce_sp_redirect_uris_for_csp([action_url_domain, *sp_redirect_uris].compact) diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb index c9695740903..cc09feb91fd 100644 --- a/app/views/idv/in_person/ready_to_verify/show.html.erb +++ b/app/views/idv/in_person/ready_to_verify/show.html.erb @@ -111,3 +111,7 @@ <% end %> + +<%= render PageFooterComponent.new do %> + <%= link_to t('in_person_proofing.body.barcode.cancel_link_text'), idv_cancel_path(step: 'verify') %> +<% end %> diff --git a/app/views/idv/shared/_ssn.html.erb b/app/views/idv/shared/_ssn.html.erb index 0bc2e473493..92ba4092df0 100644 --- a/app/views/idv/shared/_ssn.html.erb +++ b/app/views/idv/shared/_ssn.html.erb @@ -28,21 +28,19 @@ locals: <%= new_window_link_to(t('doc_auth.instructions.learn_more'), MarketingSite.security_and_privacy_practices_url) %>

    -<% if IdentityConfig.store.proofing_device_profiling_collecting_enabled %> - <% unless IdentityConfig.store.lexisnexis_threatmetrix_org_id.empty? %> - <% if threatmetrix_session_id.present? %> - <% threatmetrix_javascript_urls.each do |threatmetrix_javascript_url| %> - <%= javascript_include_tag threatmetrix_javascript_url, nonce: true %> - <% end %> - +<% if FeatureManagement.proofing_device_profiling_collecting_enabled? %> + <% if threatmetrix_session_id.present? %> + <% threatmetrix_javascript_urls.each do |threatmetrix_javascript_url| %> + <%= javascript_include_tag threatmetrix_javascript_url, nonce: true %> <% end %> + <% end %> <% end %> diff --git a/app/views/two_factor_authentication/webauthn_verification/show.html.erb b/app/views/two_factor_authentication/webauthn_verification/show.html.erb index 125a3957c16..32c901f78c4 100644 --- a/app/views/two_factor_authentication/webauthn_verification/show.html.erb +++ b/app/views/two_factor_authentication/webauthn_verification/show.html.erb @@ -21,7 +21,7 @@ <%= hidden_field_tag :authenticator_data, '', id: 'authenticator_data' %> <%= hidden_field_tag :signature, '', id: 'signature' %> <%= hidden_field_tag :client_data_json, '', id: 'client_data_json' %> - <%= hidden_field_tag :errors, '', id: 'errors' %> + <%= hidden_field_tag :webauthn_error, '', id: 'webauthn_error' %> <%= hidden_field_tag :platform, '', id: 'platform' %> <%= hidden_field_tag :webauthn_device, '', id: 'webauthn_device' %> diff --git a/bin/setup b/bin/setup index 0968289970e..374a27d01aa 100755 --- a/bin/setup +++ b/bin/setup @@ -26,7 +26,7 @@ Dir.chdir APP_ROOT do puts '== Setting up config overrides ==' default_application_yml = { 'development' => { 'config_key' => nil } } - File.write('config/application.yml', default_application_yml.to_yaml) unless File.exists?('config/application.yml') + File.write('config/application.yml', default_application_yml.to_yaml) unless File.exist?('config/application.yml') puts "== Linking service_providers.yml ==" run "test -r config/service_providers.yml || ln -sv service_providers.localdev.yml config/service_providers.yml" @@ -55,8 +55,6 @@ Dir.chdir APP_ROOT do run 'gem install foreman --conservative && gem update foreman' run "bundle check || bundle install --without deploy production" run "yarn install" - run "gem install thin -v 1.5.1 -- --with-cflags=\"-Wno-error=implicit-function-declaration\"" - run "gem install mailcatcher -- --with-cppflags=-I$(brew --prefix openssl@1.1)/include" puts "\n== Preparing database ==" run 'bin/rake db:create' diff --git a/config/application.rb b/config/application.rb index 350480e6887..c6f386ca6a9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -90,6 +90,8 @@ class Application < Rails::Application config.i18n.default_locale = :en config.action_controller.per_form_csrf_tokens = true + config.action_view.frozen_string_literal = true + routes.default_url_options[:host] = IdentityConfig.store.domain_name config.action_mailer.default_options = { diff --git a/config/application.yml.default b/config/application.yml.default index 2fb784129f9..6ae077e4a95 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -72,6 +72,7 @@ database_statement_timeout: 2_500 database_timeout: 5_000 deliver_mail_async: false deleted_user_accounts_report_configs: '[]' +development_mailer_deliver_method: letter_opener disable_csp_unsafe_inline: true disable_email_sending: true disallow_all_web_crawlers: true @@ -173,15 +174,9 @@ lexisnexis_trueid_noliveness_cropping_workflow: customers.gsa2.trueid.workflow lexisnexis_trueid_noliveness_nocropping_workflow: customers.gsa2.trueid.workflow ################################################################### # LexisNexis DDP/ThreatMetrix ##################################### -lexisnexis_threatmetrix_api_key: test_api_key -lexisnexis_threatmetrix_base_url: https://www.example.com -lexisnexis_threatmetrix_org_id: test_account -lexisnexis_threatmetrix_policy: test-policy +lexisnexis_threatmetrix_mock_enabled: true lexisnexis_threatmetrix_support_code: ABCD lexisnexis_threatmetrix_timeout: 1.0 -lexisnexis_threatmetrix_enabled: false -lexisnexis_threatmetrix_mock_enabled: true -lexisnexis_threatmetrix_required_to_verify: false lexisnexis_threatmetrix_js_signing_cert: '' ################################################################### lockout_period_in_minutes: 10 @@ -219,6 +214,7 @@ personal_key_retired: true phone_carrier_registration_blocklist: '' phone_confirmation_max_attempts: 20 phone_confirmation_max_attempt_window_in_minutes: 1_440 +phone_service_check: true phone_setups_per_ip_limit: 25 phone_setups_per_ip_period: 300 phone_setups_per_ip_track_only_mode: false @@ -234,7 +230,6 @@ piv_cac_verify_token_url: https://localhost:8443/ platform_auth_set_up_enabled: false poll_rate_for_verify_in_seconds: 3 proofer_mock_fallback: true -proofing_device_profiling_collecting_enabled: true proof_address_max_attempts: 5 proof_address_max_attempt_window_in_minutes: 360 proof_ssn_max_attempts: 10 @@ -326,7 +321,6 @@ get_usps_proofing_results_job_reprocess_delay_minutes: 5 get_usps_proofing_results_job_request_delay_milliseconds: 1000 voice_otp_pause_time: '0.5s' voice_otp_speech_rate: 'slow' -voip_check: true voip_block: true voip_allowed_phones: '[]' @@ -445,6 +439,7 @@ production: kantara_2fa_phone_restricted: false kantara_2fa_phone_existing_user_restriction: false kantara_restriction_enforcement_date: '2022-07-19' + lexisnexis_threatmetrix_mock_enabled: false logins_per_ip_limit: 20 logins_per_ip_period: 20 logins_per_ip_track_only_mode: true diff --git a/config/environments/development.rb b/config/environments/development.rb index 7996173029f..1306d3da830 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -25,8 +25,11 @@ } config.action_mailer.asset_host = IdentityConfig.store.mailer_domain_name config.action_mailer.raise_delivery_errors = false - config.action_mailer.smtp_settings = { address: ENV['SMTP_HOST'] || 'localhost', port: 1025 } config.action_mailer.show_previews = IdentityConfig.store.rails_mailer_previews_enabled + config.action_mailer.delivery_method = IdentityConfig.store.development_mailer_deliver_method + if IdentityConfig.store.development_mailer_deliver_method == :letter_opener + config.action_mailer.perform_deliveries = true + end routes.default_url_options[:protocol] = 'https' if ENV['HTTPS'] == 'on' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 2ec1d10a032..3a211b56284 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -99,6 +99,8 @@ ignore_unused: - 'errors.messages.*' - 'simple_form.*' - 'time.*' + - 'idv.failure.attempts.one' + - 'idv.failure.attempts.other' ## Exclude these keys from the `i18n-tasks eq-base' report: # ignore_eq_base: # all: diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index ebbcfb5ffb9..d260779917b 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'utf8_cleaner' Ahoy.api = false @@ -60,14 +62,8 @@ def event_logger end def invalid_uuid?(token) - # The match? method does not exist for the Regexp class in Ruby < 2.4 - # Here, it comes from Active Support. Once we upgrade to Ruby 2.5, - # we probably want to ignore the Rails definition and use Ruby's. - # To do that, we'll need to set `config.active_support.bare = true`, - # and then only require the extensions we use. token = Utf8Cleaner.new(token).remove_invalid_utf8_bytes - uuid_regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/ - !uuid_regex.match?(token) + !Idp::Constants::UUID_REGEX.match?(token) end end end diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index f4d9078d342..bfd13428d88 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do config.ssl_options = { secure_cookies: true, diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index cdd9734b7d3..bf28ed1981e 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -53,7 +53,6 @@ en: pattern_mismatch: ssn: 'Your Social Security number must be entered in as ###-##-####' zipcode: Enter a 5 or 9 digit ZIP Code - unsupported_otp_delivery_method: Select a method to receive a code. failure: attempts: one: For security reasons, you have one attempt remaining. diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index 456c3e188c3..ff78721e9bd 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -54,7 +54,6 @@ es: pattern_mismatch: ssn: 'Su número de Seguro Social debe ser ingresado como ### - ## - ####' zipcode: Ingresa un código postal de 5 o 9 dígitos - unsupported_otp_delivery_method: Seleccione una manera de recibir un código. failure: attempts: one: Por razones de seguridad, le queda un intento. diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index a4fced6deed..33ec4eab1ea 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -57,7 +57,6 @@ fr: ssn: 'Votre numéro de sécurité sociale doit être inscrit de cette façon : ###-##-####' zipcode: Entrez un code postal à 5 ou 9 chiffres - unsupported_otp_delivery_method: Sélectionnez une méthode pour recevoir un code. failure: attempts: one: Pour des raisons de sécurité, il vous reste une tentative. diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index a2294caca49..8467d8bf22d 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -7,6 +7,7 @@ en: need to bring proof of your current address to the Post Office. learn_more: Learn more barcode: + cancel_link_text: Cancel your barcode close_window: You may now close this window. deadline: Visit the Post Office by %{deadline}. deadline_restart: If you go after the deadline, your information will not be diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index 7f6d98fd65e..faeda07b5dc 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -8,6 +8,7 @@ es: dirección actual. learn_more: Aprende más barcode: + cancel_link_text: Cancelar su código de barras close_window: Ya puede cerrar esta ventana deadline: Visite la Oficina de correos antes del %{deadline} deadline_restart: Si supera la fecha límite, su información no se guardará y diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index a26a6e042c1..6ad333ff440 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -8,6 +8,7 @@ fr: actuelle au bureau de poste. learn_more: Apprendre encore plus barcode: + cancel_link_text: Annulez votre code-barres close_window: Vous pouvez maintenant fermer cette fenêtre deadline: Visitez le bureau de poste d’ici %{deadline} deadline_restart: Si vous partez après la date limite, vos informations ne diff --git a/config/locales/step_indicator/en.yml b/config/locales/step_indicator/en.yml index 4867d45caac..c8607146f30 100644 --- a/config/locales/step_indicator/en.yml +++ b/config/locales/step_indicator/en.yml @@ -16,4 +16,3 @@ en: complete: Completed current: Current step not_complete: Not completed - pending: Pending diff --git a/config/locales/step_indicator/es.yml b/config/locales/step_indicator/es.yml index d878afd0bad..1d207d3ee07 100644 --- a/config/locales/step_indicator/es.yml +++ b/config/locales/step_indicator/es.yml @@ -16,4 +16,3 @@ es: complete: Completo current: Siguiente paso not_complete: No se ha completado - pending: Pendiente diff --git a/config/locales/step_indicator/fr.yml b/config/locales/step_indicator/fr.yml index 3323146f8dc..4eca9bc6134 100644 --- a/config/locales/step_indicator/fr.yml +++ b/config/locales/step_indicator/fr.yml @@ -16,4 +16,3 @@ fr: complete: Terminé current: Étape en cours not_complete: Non terminé - pending: En attente diff --git a/docker-compose-production.yml b/docker-compose-production.yml index 25d12dae549..87dd2fbd340 100644 --- a/docker-compose-production.yml +++ b/docker-compose-production.yml @@ -30,12 +30,9 @@ services: DOCKER_DB_USER: 'postgres' # '' == 1 thread for tests; performs better in a container TEST_ENV_NUMBER: '' - SMTP_HOST: 'mailcatcher' depends_on: - db - redis - # - web - - mailcatcher web: image: nginx:alpine ports: @@ -49,8 +46,3 @@ services: POSTGRES_HOST_AUTH_METHOD: 'trust' redis: image: redis:5-alpine - mailcatcher: - image: rordi/docker-mailcatcher - container_name: mailcatcher - ports: - - 1080:1080 diff --git a/docker-compose.yml b/docker-compose.yml index fb183431508..6402ca3441d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,12 +25,10 @@ services: DOCKER_DB_USER: 'postgres' # '' == 1 thread for tests; performs better in a container TEST_ENV_NUMBER: '' - SMTP_HOST: 'mailcatcher' NODE_ENV: 'development' depends_on: - db - redis - - mailcatcher tty: true stdin_open: true db: @@ -42,8 +40,3 @@ services: POSTGRES_HOST_AUTH_METHOD: 'trust' redis: image: redis:5-alpine - mailcatcher: - image: rordi/docker-mailcatcher - container_name: mailcatcher - ports: - - 1080:1080 diff --git a/lib/asset_sources.rb b/lib/asset_sources.rb index 1e2579d4922..8766571fbc4 100644 --- a/lib/asset_sources.rb +++ b/lib/asset_sources.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AssetSources class << self attr_accessor :manifest_path diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 9e82e5dfca5..14df655716a 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -116,4 +116,41 @@ def self.voip_allowed_phones allowed_phones.map { |p| Phonelib.parse(p).e164 }.to_set end end + + # Whether we collect device profiling information as part of the proofing process. + def self.proofing_device_profiling_collecting_enabled? + case IdentityConfig.store.proofing_device_profiling + when :enabled, :collect_only then true + when :disabled then false + # BEGIN temporary transitional fallback + when nil + if IdentityConfig.store.proofing_device_profiling_collecting_enabled.nil? + false + else + IdentityConfig.store.proofing_device_profiling_collecting_enabled + end + # END temporary transitional fallback + else + raise 'Invalid value for proofing_device_profiling' + end + end + + # Whether we prevent users from proceeding with identity verification based on the outcomes of + # device profiling. + def self.proofing_device_profiling_decisioning_enabled? + case IdentityConfig.store.proofing_device_profiling + when :enabled then true + when :collect_only, :disabled then false + # BEGIN temporary transitional fallback + when nil + if IdentityConfig.store.lexisnexis_threatmetrix_required_to_verify.nil? + false + else + IdentityConfig.store.lexisnexis_threatmetrix_required_to_verify + end + # END temporary transitional fallback + else + raise 'Invalid value for proofing_device_profiling' + end + end end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index fab027d7b62..1e6b6bdaaad 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -60,7 +60,7 @@ def add(key, type: :string, allow_nil: false, enum: nil, options: {}) converted_value = CONVERTERS.fetch(type).call(value, options: options) if !value.nil? raise "#{key} is required but is not present" if converted_value.nil? && !allow_nil - if enum && !enum.include?(converted_value) + if enum && !(enum.include?(converted_value) || (converted_value.nil? && allow_nil)) raise "unexpected #{key}: #{value}, expected one of #{enum}" end @@ -143,6 +143,7 @@ def self.build_store(config_map) config.add(:database_worker_jobs_password, type: :string) config.add(:deleted_user_accounts_report_configs, type: :json) config.add(:deliver_mail_async, type: :boolean) + config.add(:development_mailer_deliver_method, type: :symbol, enum: [:file, :letter_opener]) config.add(:disable_csp_unsafe_inline, type: :boolean) config.add(:disable_email_sending, type: :boolean) config.add(:disallow_all_web_crawlers, type: :boolean) @@ -248,13 +249,13 @@ def self.build_store(config_map) config.add(:lexisnexis_trueid_noliveness_cropping_workflow, type: :string) config.add(:lexisnexis_trueid_noliveness_nocropping_workflow, type: :string) config.add(:lexisnexis_trueid_timeout, type: :float) - config.add(:lexisnexis_threatmetrix_api_key, type: :string) - config.add(:lexisnexis_threatmetrix_base_url, type: :string) - config.add(:lexisnexis_threatmetrix_enabled, type: :boolean) + config.add(:lexisnexis_threatmetrix_api_key, type: :string, allow_nil: true) + config.add(:lexisnexis_threatmetrix_base_url, type: :string, allow_nil: true) + config.add(:lexisnexis_threatmetrix_enabled, type: :boolean, allow_nil: true) config.add(:lexisnexis_threatmetrix_mock_enabled, type: :boolean) - config.add(:lexisnexis_threatmetrix_org_id, type: :string) - config.add(:lexisnexis_threatmetrix_policy, type: :string) - config.add(:lexisnexis_threatmetrix_required_to_verify, type: :boolean) + config.add(:lexisnexis_threatmetrix_org_id, type: :string, allow_nil: true) + config.add(:lexisnexis_threatmetrix_policy, type: :string, allow_nil: true) + config.add(:lexisnexis_threatmetrix_required_to_verify, type: :boolean, allow_nil: true) config.add(:lexisnexis_threatmetrix_support_code, type: :string) config.add(:lexisnexis_threatmetrix_timeout, type: :float) config.add(:lexisnexis_threatmetrix_js_signing_cert, type: :string) @@ -319,11 +320,18 @@ def self.build_store(config_map) config.add(:piv_cac_service_timeout, type: :float) config.add(:piv_cac_verify_token_secret) config.add(:piv_cac_verify_token_url) + config.add(:phone_service_check, type: :boolean) config.add(:phone_setups_per_ip_track_only_mode, type: :boolean) config.add(:platform_auth_set_up_enabled, type: :boolean) config.add(:poll_rate_for_verify_in_seconds, type: :integer) config.add(:proofer_mock_fallback, type: :boolean) - config.add(:proofing_device_profiling_collecting_enabled, type: :boolean) + config.add( + :proofing_device_profiling, + type: :symbol, + enum: [:disabled, :collect_only, :enabled], + allow_nil: true, + ) + config.add(:proofing_device_profiling_collecting_enabled, type: :boolean, allow_nil: true) config.add(:proof_address_max_attempts, type: :integer) config.add(:proof_address_max_attempt_window_in_minutes, type: :integer) config.add(:proof_ssn_max_attempts, type: :integer) @@ -425,7 +433,6 @@ def self.build_store(config_map) config.add(:voice_otp_speech_rate) config.add(:voip_allowed_phones, type: :json) config.add(:voip_block, type: :boolean) - config.add(:voip_check, type: :boolean) @key_types = config.key_types @store = RedactedStruct.new('IdentityConfig', *config.written_env.keys, keyword_init: true). diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index f3b8f77c5dd..760795e4831 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -1,5 +1,6 @@ module Idp module Constants + UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/ module Vendors ACUANT = 'acuant' LEXIS_NEXIS = 'lexis_nexis' diff --git a/lib/rack_request_parser.rb b/lib/rack_request_parser.rb index ff174639aea..49ce9d43d6a 100644 --- a/lib/rack_request_parser.rb +++ b/lib/rack_request_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RackRequestParser attr_reader :request diff --git a/lib/secure_cookies.rb b/lib/secure_cookies.rb index dc0cf0e83aa..b432ba90cce 100644 --- a/lib/secure_cookies.rb +++ b/lib/secure_cookies.rb @@ -1,6 +1,11 @@ +# frozen_string_literal: true + # Reimplements SecureHeaders secure cookie functionality to make sure all cookies are secure class SecureCookies - COOKIE_SEPARATOR = "\n".freeze + COOKIE_SEPARATOR = "\n" + SECURE_REGEX = /; Secure/i + HTTP_ONLY_REGEX = /; HttpOnly/i + SAME_SITE_REGEX = /; SameSite/i def initialize(app) @app = app @@ -15,9 +20,9 @@ def call(env) cookies.each do |cookie| next if cookie.blank? - cookie << '; Secure' if env['HTTPS'] == 'on' && !cookie.match?(/; Secure/i) - cookie << '; HttpOnly' if !cookie.match?(/; HttpOnly/i) - cookie << '; SameSite=Lax' if !cookie.match?(/; SameSite/i) + cookie << '; Secure' if env['HTTPS'] == 'on' && !cookie.match?(SECURE_REGEX) + cookie << '; HttpOnly' if !cookie.match?(HTTP_ONLY_REGEX) + cookie << '; SameSite=Lax' if !cookie.match?(SAME_SITE_REGEX) end headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR) diff --git a/lib/session_encryptor.rb b/lib/session_encryptor.rb index c62a1081b17..57aae4a88cc 100644 --- a/lib/session_encryptor.rb +++ b/lib/session_encryptor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SessionEncryptor class SensitiveKeyError < StandardError; end diff --git a/lib/tasks/create_test_accounts.rb b/lib/tasks/create_test_accounts.rb index 601b9d73212..aff3634fc56 100644 --- a/lib/tasks/create_test_accounts.rb +++ b/lib/tasks/create_test_accounts.rb @@ -59,7 +59,7 @@ def create_account(email: 'joe.smith@email.com', password: 'salty pickles', mfa_ def create_accounts_from_csv(data) require 'csv' accounts_created = [] - users = File.exists?(data) ? CSV.read(data, headers: true) : CSV.parse(data, headers: true) + users = File.exist?(data) ? CSV.read(data, headers: true) : CSV.parse(data, headers: true) users.each do |row| email = row['email'] if User.find_with_email(email).present? diff --git a/lib/utf8_cleaner.rb b/lib/utf8_cleaner.rb index e49d806c7d7..2893092d802 100644 --- a/lib/utf8_cleaner.rb +++ b/lib/utf8_cleaner.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Utf8Cleaner attr_reader :string diff --git a/lib/utf8_sanitizer.rb b/lib/utf8_sanitizer.rb index fca5f96d50c..965da0067ad 100644 --- a/lib/utf8_sanitizer.rb +++ b/lib/utf8_sanitizer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack_request_parser' require 'utf8_cleaner' diff --git a/spec/components/download_button_component_spec.rb b/spec/components/download_button_component_spec.rb index 25ebf2cd99c..8ca1cd9d5e0 100644 --- a/spec/components/download_button_component_spec.rb +++ b/spec/components/download_button_component_spec.rb @@ -15,7 +15,6 @@ subject(:rendered) { render_inline instance } it 'renders link with data and file name' do - expect(rendered).to have_css('lg-download-button') expect(rendered).to have_css( "a[href='data:text/plain;charset=utf-8,Downloaded%20Text'][download='#{file_name}']", text: t('components.download_button.label'), diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b9af9605ac3..f3fd32a8f97 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -406,6 +406,13 @@ def index it 'returns the saml completion url' do expect(url_with_updated_params).to eq complete_saml_url end + + context 'updates the sp_session to mark the final auth request' do + it 'updates the sp_session to mark the final auth request' do + url_with_updated_params + expect(controller.session[:sp][:final_auth_request]).to be true + end + end end context 'with an OIDC request' do diff --git a/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb b/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb index a6ea567e454..e3b1f066f04 100644 --- a/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb +++ b/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb @@ -15,8 +15,8 @@ def index; end let(:ff_enabled) { true } before do - allow(IdentityConfig.store).to receive(:proofing_device_profiling_collecting_enabled). - and_return(ff_enabled) + allow(IdentityConfig.store).to receive(:proofing_device_profiling). + and_return(ff_enabled ? :enabled : :disabled) end context 'ff is set' do diff --git a/spec/controllers/idv/gpo_verify_controller_spec.rb b/spec/controllers/idv/gpo_verify_controller_spec.rb index fe6bfded679..b766190954d 100644 --- a/spec/controllers/idv/gpo_verify_controller_spec.rb +++ b/spec/controllers/idv/gpo_verify_controller_spec.rb @@ -30,10 +30,8 @@ allow(decorated_user).to receive(:pending_profile_requires_verification?). and_return(has_pending_profile) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled). - and_return(threatmetrix_enabled) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(threatmetrix_enabled) + allow(IdentityConfig.store).to receive(:proofing_device_profiling). + and_return(threatmetrix_enabled ? :enabled : :disabled) end describe '#index' do diff --git a/spec/controllers/idv/otp_delivery_method_controller_spec.rb b/spec/controllers/idv/otp_delivery_method_controller_spec.rb deleted file mode 100644 index 31334b47a05..00000000000 --- a/spec/controllers/idv/otp_delivery_method_controller_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -require 'rails_helper' - -describe Idv::OtpDeliveryMethodController do - let(:user) { build(:user) } - let(:valid_phone_number) { '+1 (225) 555-5000' } - - before do - stub_verify_steps_one_and_two(user) - subject.idv_session.address_verification_mechanism = 'phone' - subject.idv_session.vendor_phone_confirmation = true - subject.idv_session.user_phone_confirmation = false - user_phone_confirmation_session = Idv::PhoneConfirmationSession.start( - phone: valid_phone_number, - delivery_method: :sms, - ) - subject.idv_session.user_phone_confirmation_session = user_phone_confirmation_session - end - - describe 'before_actions' do - it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) - end - end - - describe '#new' do - context 'user has not selected phone verification method' do - before do - subject.idv_session.address_verification_mechanism = 'gpo' - end - - it 'redirects to the review controller' do - get :new - expect(response).to redirect_to idv_review_path - end - end - - context 'user has confirmed phone number' do - before do - subject.idv_session.user_phone_confirmation = true - end - - it 'redirects to the review controller' do - get :new - expect(response).to redirect_to idv_review_path - end - end - - context 'user has not completed phone step' do - before do - subject.idv_session.vendor_phone_confirmation = false - end - - it 'redirects to the phone controller' do - get :new - expect(response).to redirect_to idv_phone_path - end - end - - context 'user has selected phone verification and not confirmed phone' do - it 'renders' do - get :new - expect(response).to render_template :new - end - end - - it 'tracks an analytics event' do - stub_analytics - allow(@analytics).to receive(:track_event) - - get :new - - expect(@analytics).to have_received(:track_event). - with('IdV: Phone OTP delivery Selection Visited', proofing_components: nil) - end - end - - describe '#create' do - let(:params) { { otp_delivery_preference: :sms } } - let(:valid_phone_parameter) { { phone_number: valid_phone_number } } - let(:success_parameters) { { failure_reason: {}, success: true } } - let(:defalut_parameters) { { **valid_phone_parameter, otp_delivery_method: 'sms' } } - - context 'user has not selected phone verification method' do - before do - subject.idv_session.address_verification_mechanism = 'gpo' - end - - it 'redirects to the review controller' do - post :create, params: params - expect(response).to redirect_to idv_review_path - end - end - - context 'user has confirmed phone number' do - before do - subject.idv_session.user_phone_confirmation = true - end - - it 'redirects to the review controller' do - post :create, params: params - expect(response).to redirect_to idv_review_path - end - end - - context 'user has not completed phone step' do - before do - subject.idv_session.vendor_phone_confirmation = false - end - - it 'redirects to the phone controller' do - post :create, params: params - expect(response).to redirect_to idv_phone_path - end - end - - context 'user has selected sms' do - it 'redirects to the otp send path for sms' do - post :create, params: params - expect(subject.idv_session.user_phone_confirmation_session.delivery_method).to eq(:sms) - expect(response).to redirect_to idv_otp_verification_path - end - - it 'tracks appropriate events' do - stub_analytics - stub_attempts_tracker - allow(@analytics).to receive(:track_event) - - expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_sent).with( - { **success_parameters, **defalut_parameters }, - ) - - post :create, params: params - - result = { - success: true, - errors: {}, - otp_delivery_preference: 'sms', - } - - expect(@analytics).to have_received(:track_event). - with('IdV: Phone OTP Delivery Selection Submitted', result) - end - end - - context 'user has selected voice' do - let(:params) { { otp_delivery_preference: :voice } } - - it 'redirects to the otp send path for voice' do - post :create, params: params - expect(subject.idv_session.user_phone_confirmation_session.delivery_method).to eq(:voice) - expect(response).to redirect_to idv_otp_verification_path - end - - it 'tracks appropriate events' do - stub_analytics - stub_attempts_tracker - allow(@analytics).to receive(:track_event) - - expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_sent).with( - { **success_parameters, - **valid_phone_parameter, - otp_delivery_method: 'voice' }, - ) - - post :create, params: params - - result = { - success: true, - errors: {}, - otp_delivery_preference: 'voice', - } - - expect(@analytics).to have_received(:track_event). - with('IdV: Phone OTP Delivery Selection Submitted', result) - end - end - - context 'form is invalid' do - let(:params) { { otp_delivery_preference: :🎷 } } - - it 'renders the new template' do - post :create, params: params - expect(response).to render_template :new - end - - it 'tracks appropriate events' do - stub_analytics - stub_attempts_tracker - allow(@analytics).to receive(:track_event) - - expect(@irs_attempts_api_tracker).not_to receive(:idv_phone_otp_sent) - - post :create, params: params - - result = { - success: false, - errors: { otp_delivery_preference: ['is not included in the list'] }, - error_details: { otp_delivery_preference: [:inclusion] }, - otp_delivery_preference: '🎷', - } - - expect(@analytics).to have_received(:track_event). - with('IdV: Phone OTP Delivery Selection Submitted', result) - end - end - - context 'the telephony gem raises an exception' do - let(:telephony_error_analytics_hash) do - { - error: 'Telephony::TelephonyError', - message: 'error message', - context: 'idv', - country: 'US', - } - end - let(:telephony_error) do - Telephony::TelephonyError.new('error message') - end - let(:telephony_response) do - Telephony::Response.new( - success: false, - error: telephony_error, - extra: { request_id: 'error-request-id' }, - ) - end - - before do - stub_analytics - stub_attempts_tracker - allow(Telephony).to receive(:send_confirmation_otp).and_return(telephony_response) - end - - it 'tracks an analytics events' do - expect(@analytics).to receive(:track_event).ordered.with( - 'IdV: Phone OTP Delivery Selection Submitted', hash_including(success: true) - ) - expect(@analytics).to receive(:track_event).ordered.with( - 'IdV: phone confirmation otp sent', - hash_including( - success: false, - adapter: :test, - telephony_response: telephony_response, - ), - ) - expect(@analytics).to receive(:track_event).ordered.with( - 'Vendor Phone Validation failed', telephony_error_analytics_hash - ) - - expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_sent).with( - **defalut_parameters, - success: false, - failure_reason: { telephony_error: I18n.t('telephony.error.friendly_message.generic') }, - ) - - post :create, params: params - end - end - end -end diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index 02a468d1b2c..4a26b0a066b 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -236,8 +236,7 @@ def index context 'with device profiling decisioning enabled' do before do ProofingComponent.create(user: user, threatmetrix: true, threatmetrix_review_status: nil) - allow(IdentityConfig.store). - to receive(:lexisnexis_threatmetrix_required_to_verify).and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) end context 'threatmetrix review status is nil' do @@ -282,6 +281,7 @@ def index context 'device profiling fails' do before do ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'reject') + profile.active = false profile.deactivation_reason = :threatmetrix_review_pending end diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb index 82d1289c759..710ee00625b 100644 --- a/spec/controllers/idv/review_controller_spec.rb +++ b/spec/controllers/idv/review_controller_spec.rb @@ -600,10 +600,7 @@ def show threatmetrix: true, threatmetrix_review_status: 'review', ) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled). - and_return(true) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) end it 'creates a disabled profile' do diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 57dd0fbcf3c..935d220d9a1 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -873,17 +873,50 @@ def name_id_version(format_urn) end end - context 'ForceAuthn set to true' do - it 'signs the user out if a session is active' do - user = create(:user, :signed_up) + context 'saml_internal_post feature configuration is set to true with ForceAuthn' do + let(:user) { create(:user, :signed_up) } + + it 'signs user out if a session is active and sp_session[:final_auth_request] is falsey' do sign_in(user) generate_saml_response(user, saml_settings(overrides: { force_authn: true })) - # would be 200 if the user's session persists expect(response.status).to eq(302) # implicit test of request storage since request_id would be missing otherwise expect(response.location).to match(%r{#{root_url}\?request_id=.+}) end + + it 'skips signing out the user when sp_session[:final_auth_request] is true' do + link_user_to_identity(user, true, saml_settings(overrides: { force_authn: true })) + sign_in(user) + controller.session[:sp] = { final_auth_request: true } + saml_final_post_auth(saml_request(saml_settings(overrides: { force_authn: true }))) + expect(response).to_not be_redirect + expect(response.status).to eq(200) + end + + it 'sets sp_session[:final_auth_request] to false before returning' do + sign_in(user) + controller.session[:sp] = { final_auth_request: true } + saml_final_post_auth(saml_request(saml_settings(overrides: { force_authn: true }))) + expect(session[:sp][:final_auth_request]).to be_falsey + end + end + + context 'saml_internal_post feature configuration is set to false' do + let(:user) { create(:user, :signed_up) } + + before { allow(IdentityConfig.store).to receive(:saml_internal_post).and_return(false) } + + context 'ForceAuthn set to true' do + it 'signs the user out if a session is active' do + sign_in(user) + generate_saml_response(user, saml_settings(overrides: { force_authn: true })) + # would be 200 if the user's session persists + expect(response.status).to eq(302) + # implicit test of request storage since request_id would be missing otherwise + expect(response.location).to match(%r{#{root_url}\?request_id=.+}) + end + end end context 'service provider is inactive' do @@ -1582,7 +1615,7 @@ def name_id_version(format_urn) end it 'includes an ID attribute with a valid UUID' do - expect(UUID.validate(assertion['ID'][1..-1])).to eq(true) + expect(Idp::Constants::UUID_REGEX.match?(assertion['ID'][1..-1])).to eq(true) expect(assertion['ID']).to eq "_#{user.last_identity.session_uuid}" end @@ -1705,7 +1738,7 @@ def name_id_version(format_urn) end it 'includes a URI attribute' do - expect(UUID.validate(reference['URI'][2..-1])).to eq(true) + expect(Idp::Constants::UUID_REGEX.match?(reference['URI'][2..-1])).to eq(true) end end end 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 150c646f14e..67eedb26e5d 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -77,7 +77,6 @@ ) allow(WebauthnVerificationForm).to receive(:domain_name).and_return('localhost:3000') result = { context: 'authentication', - errors: {}, multi_factor_auth_method: 'webauthn', success: true, webauthn_configuration_id: webauthn_configuration.id } @@ -104,7 +103,6 @@ ) allow(WebauthnVerificationForm).to receive(:domain_name).and_return('localhost:3000') result = { context: 'authentication', - errors: {}, multi_factor_auth_method: 'webauthn_platform', success: true, webauthn_configuration_id: WebauthnConfiguration.first.id } @@ -130,9 +128,9 @@ ) result = { context: 'authentication', - errors: {}, multi_factor_auth_method: 'webauthn', success: false, + error_details: { authenticator_data: [:invalid_authenticator_data] }, webauthn_configuration_id: webauthn_configuration.id } expect(@analytics).to receive(:track_mfa_submit_event). with(result) @@ -141,6 +139,7 @@ end context 'webauthn_platform returns an error from frontend API' do + let(:webauthn_error) { 'NotAllowedError' } let(:params) do { authenticator_data: authenticator_data, @@ -148,7 +147,7 @@ signature: signature, credential_id: credential_id, platform: true, - errors: 'cannot sign in properly', + webauthn_error: webauthn_error, } end @@ -162,6 +161,13 @@ allow_any_instance_of(TwoFactorAuthCode::WebauthnAuthenticationPresenter). to receive(:multiple_factors_enabled?). and_return(true) + create( + :webauthn_configuration, + user: controller.current_user, + credential_id: credential_id, + credential_public_key: credential_public_key, + platform_authenticator: true, + ) end it 'redirects to webauthn show page' do @@ -179,6 +185,20 @@ ), ) end + + it 'logs an event with error details' do + expect(@analytics).to receive(:track_mfa_submit_event).with( + hash_including( + success: false, + error_details: { webauthn_error: [webauthn_error] }, + context: UserSessionContext::AUTHENTICATION_CONTEXT, + multi_factor_auth_method: 'webauthn_platform', + webauthn_configuration_id: controller.current_user.webauthn_configurations.first.id, + ), + ) + + patch :confirm, params: params + end end context 'User only has webauthn as an MFA method' do diff --git a/spec/controllers/users/phone_setup_controller_spec.rb b/spec/controllers/users/phone_setup_controller_spec.rb index fa6742024db..12385393b60 100644 --- a/spec/controllers/users/phone_setup_controller_spec.rb +++ b/spec/controllers/users/phone_setup_controller_spec.rb @@ -2,7 +2,7 @@ describe Users::PhoneSetupController do before do - allow(IdentityConfig.store).to receive(:voip_check).and_return(true) + allow(IdentityConfig.store).to receive(:phone_service_check).and_return(true) allow(IdentityConfig.store).to receive(:voip_block).and_return(true) end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 4b4a7c6fa08..15200f324f4 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -639,6 +639,22 @@ get :new, params: { user: 'this_is_not_a_hash' } end.to_not raise_error end + + context 'with prefilled email/password via url params' do + render_views + + it 'does not prefill the form' do + email = Faker::Internet.safe_email + password = SecureRandom.uuid + + get :new, params: { user: { email: email, password: password } } + + doc = Nokogiri::HTML(response.body) + + expect(doc.at_css('input[name="user[email]"]')[:value]).to be_nil + expect(doc.at_css('input[name="user[password]"]')[:value]).to be_nil + end + end end describe 'POST /sessions/keepalive' do diff --git a/spec/features/idv/doc_auth/ssn_step_spec.rb b/spec/features/idv/doc_auth/ssn_step_spec.rb index de50dac54e4..e249f80d6e3 100644 --- a/spec/features/idv/doc_auth/ssn_step_spec.rb +++ b/spec/features/idv/doc_auth/ssn_step_spec.rb @@ -6,6 +6,9 @@ include DocCaptureHelper before do + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('test_org') + sign_in_and_2fa_user complete_doc_auth_steps_before_ssn_step end diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 839b6cc7b2f..22ac2b005e6 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -14,9 +14,8 @@ let(:user) { user_with_2fa } before do - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled).and_return(true) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('test_org') end it 'allows the user to continue down the happy path', allow_browser_log: true do diff --git a/spec/features/idv/steps/gpo_otp_verification_step_spec.rb b/spec/features/idv/steps/gpo_otp_verification_step_spec.rb index 3839c2e690f..9c8d2ff00b8 100644 --- a/spec/features/idv/steps/gpo_otp_verification_step_spec.rb +++ b/spec/features/idv/steps/gpo_otp_verification_step_spec.rb @@ -24,24 +24,26 @@ end let(:user) { profile.user } let(:threatmetrix_enabled) { false } - let(:threatmetrix_required_to_verify) { false } let(:threatmetrix_review_status) { nil } let(:redirect_after_verification) { nil } let(:profile_should_be_active) { true } let(:expected_deactivation_reason) { nil } before do - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled). - and_return(threatmetrix_enabled) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(threatmetrix_required_to_verify) + allow(IdentityConfig.store).to receive(:proofing_device_profiling). + and_return(threatmetrix_enabled ? :enabled : :disabled) end it_behaves_like 'gpo otp verification' + context 'ThreatMetrix disabled, but we have ThreatMetrix status on proofing component' do + let(:threatmetrix_enabled) { false } + let(:threatmetrix_review_status) { 'review' } + it_behaves_like 'gpo otp verification' + end + context 'ThreatMetrix enabled' do let(:threatmetrix_enabled) { true } - let(:threatmetrix_required_to_verify) { true } context 'ThreatMetrix says "pass"' do let(:threatmetrix_review_status) { 'pass' } @@ -68,16 +70,6 @@ let(:threatmetrix_review_status) { nil } it_behaves_like 'gpo otp verification' end - - context 'without verification requirement enabled creates active profile' do - let(:threatmetrix_required_to_verify) { false } - - let(:threatmetrix_review_status) { 'review' } - let(:redirect_after_verification) { account_path } # TODO - let(:profile_should_be_active) { true } - let(:expected_deactivation_reason) { nil } - it_behaves_like 'gpo otp verification' - end end context 'with gpo feature disabled' do diff --git a/spec/features/idv/threatmetrix_pending_spec.rb b/spec/features/idv/threatmetrix_pending_spec.rb index 1ba1bbaccb5..d59fa5c1d13 100644 --- a/spec/features/idv/threatmetrix_pending_spec.rb +++ b/spec/features/idv/threatmetrix_pending_spec.rb @@ -4,9 +4,8 @@ include IdvStepHelper before do - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled).and_return(true) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('test_org') end scenario 'users pending threatmetrix see sad face screen and cannot perform idv' do diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb index a77c6022e1f..554f9dd24fc 100644 --- a/spec/features/phone/add_phone_spec.rb +++ b/spec/features/phone/add_phone_spec.rb @@ -157,7 +157,7 @@ scenario 'adding a VOIP phone' do allow(IdentityConfig.store).to receive(:voip_block).and_return(true) - allow(IdentityConfig.store).to receive(:voip_check).and_return(true) + allow(IdentityConfig.store).to receive(:phone_service_check).and_return(true) user = create(:user, :signed_up) diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 4b190e096e2..0d6c2cd56c3 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -1,7 +1,5 @@ require 'rails_helper' -class MockSession; end - feature 'saml api' do include SamlAuthHelper include IdvHelper @@ -235,6 +233,166 @@ class MockSession; end end end + context 'with an SP configured to receive verified attributes' do + context 'with a proofed user' do + let(:pii) { { phone: '+12025555555', ssn: '111111111', dob: '01/01/1941' } } + let(:user) { create(:profile, :active, :verified, pii: pii).user } + + scenario 'sign in flow with user authorizing SP' do + visit_idp_from_saml_sp_with_ial2 + sign_in_live_with_2fa(user) + click_submit_default + click_agree_and_continue + click_submit_default_twice + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.attribute_value_for(:ial)).to eq( + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + ) + + expect { xmldoc.attribute_value_for(:ssn) }.not_to raise_exception + expect(xmldoc.attribute_value_for(:ssn)).to eq('111111111') + + sp_return_logs = SpReturnLog.where(user_id: user.id) + expect(sp_return_logs.count).to eq(1) + expect(sp_return_logs.first.ial).to eq(2) + end + context 'when ForceAuthn = true in SAMLRequest' do + let(:saml_request_overrides) do + { + issuer: sp1_issuer, + authn_context: [ + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}\ + first_name:last_name email, ssn", + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", + ], + force_authn: true, + security: { + embed_sign: false, + }, + } + end + + scenario 'enforces reauthentication if already signed in' do + # start with an active user session + sign_in_live_with_2fa(user) + + # visit from SP with force_authn: true + visit_saml_authn_request_url(overrides: saml_request_overrides) + expect(page).to have_content( + 'is using Login.gov to allow you to sign in to your account safely and securely.', + ) + expect(page).to have_button('Sign in') + + # sign in again + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + click_agree_and_continue + click_submit_default_twice + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + + expect(xmldoc.attribute_value_for(:ial)).to eq( + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + ) + expect { xmldoc.attribute_value_for(:ssn) }.not_to raise_exception + expect(xmldoc.attribute_value_for(:ssn)).to eq('111111111') + expect( + xmldoc.status_code.attribute('Value').value, + ).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success' + + sp_return_logs = SpReturnLog.where(user_id: user.id) + expect(sp_return_logs.count).to eq(1) + expect(sp_return_logs.first.ial).to eq(2) + end + + scenario 'enforces reauthentication if already signed in from the same SP' do + # first visit from Test SP + visit_saml_authn_request_url(overrides: saml_request_overrides) + expect(page).to have_content( + 'Test SP is using Login.gov to allow you to sign in' \ + ' to your account safely and securely.', + ) + expect(page).to have_button('Sign in') + # Log in with Test SP as the SP session + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + click_agree_and_continue + click_submit_default_twice + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect( + xmldoc.status_code.attribute('Value').value, + ).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success' + + # second visit to log in from the same SP as before, should be signed out + # because ForceAuthn = true even though the user session would still be active + # for Test SP + visit_saml_authn_request_url(overrides: saml_request_overrides) + expect(page).to have_content( + 'Test SP is using Login.gov to allow you to sign in' \ + ' to your account safely and securely.', + ) + expect(page).to have_button('Sign in') + + # log in for second time + fill_in_credentials_and_submit(user.email, user.password) + click_submit_default_twice + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect( + xmldoc.status_code.attribute('Value').value, + ).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success' + end + end + end + + context 'with an IAL1 SP' do + scenario 'sign in flow with user already linked to SP' do + link_user_to_identity(user, true, saml_settings) + visit_idp_from_sp_with_ial1(:saml) + sign_in_live_with_2fa(user) + click_submit_default_twice + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.attribute_value_for(:ial)).to eq( + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + ) + expect( + xmldoc.status_code.attribute('Value').value, + ).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success' + end + + scenario 'enforces reauthentication when ForceAuthn = true in SAMLRequest' do + # start with an active user session + sign_in_live_with_2fa(user) + + # visit from SP with force_authn: true + visit_saml_authn_request_url(overrides: { force_authn: true }) + expect(page).to have_content( + 'is using Login.gov to allow you to sign in to your account safely and securely.', + ) + expect(page).to have_button('Sign in') + + # sign in again + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + click_agree_and_continue + click_submit_default_twice + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.attribute_value_for(:ial)).to eq( + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + ) + expect( + xmldoc.status_code.attribute('Value').value, + ).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success' + end + end + end + context 'when sending POST request to /api/saml/auth/' do it 'logs one SAML Auth Requested event and multiple SAML Auth events for IAL1 request' do fake_analytics = FakeAnalytics.new diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb index fa8a6c79c23..df5348f6d73 100644 --- a/spec/forms/gpo_verify_form_spec.rb +++ b/spec/forms/gpo_verify_form_spec.rb @@ -159,10 +159,7 @@ let(:threatmetrix_review_status) { 'reject' } before do - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled). - and_return(true) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) end it 'returns true' do @@ -184,8 +181,7 @@ context 'threatmetrix is not required for verification' do before do - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). - and_return(false) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:disabled) end it 'returns true' do diff --git a/spec/forms/new_phone_form_spec.rb b/spec/forms/new_phone_form_spec.rb index ea8ea60cb66..9a1a9a561f1 100644 --- a/spec/forms/new_phone_form_spec.rb +++ b/spec/forms/new_phone_form_spec.rb @@ -185,7 +185,7 @@ end before do - allow(IdentityConfig.store).to receive(:voip_check).and_return(true) + allow(IdentityConfig.store).to receive(:phone_service_check).and_return(true) expect(Telephony).to receive(:phone_info).with(phone). and_raise(Aws::Pinpoint::Errors::BadRequestException.new(nil, nil)) @@ -209,11 +209,11 @@ context 'voip numbers' do let(:telephony_gem_voip_number) { '+12255552000' } let(:voip_block) { false } - let(:voip_check) { true } + let(:phone_service_check) { true } before do allow(IdentityConfig.store).to receive(:voip_block).and_return(voip_block) - allow(IdentityConfig.store).to receive(:voip_check).and_return(voip_check) + allow(IdentityConfig.store).to receive(:phone_service_check).and_return(phone_service_check) end subject(:result) do @@ -262,7 +262,7 @@ end context 'when voip checks are disabled' do - let(:voip_check) { false } + let(:phone_service_check) { false } it 'does not check the phone type' do expect(Telephony).to_not receive(:phone_info) @@ -289,7 +289,7 @@ end context 'when voip checks are disabled' do - let(:voip_check) { false } + let(:phone_service_check) { false } it 'does not check the phone type' do expect(Telephony).to_not receive(:phone_info) diff --git a/spec/forms/webauthn_verification_form_spec.rb b/spec/forms/webauthn_verification_form_spec.rb index 9fd4c636d4a..1760baa1beb 100644 --- a/spec/forms/webauthn_verification_form_spec.rb +++ b/spec/forms/webauthn_verification_form_spec.rb @@ -4,86 +4,195 @@ include WebAuthnHelper let(:user) { create(:user) } - let(:user_session) { { webauthn_challenge: webauthn_challenge } } - let(:subject) { WebauthnVerificationForm.new(user, user_session) } - let(:platform_authenticator) { nil } + let(:challenge) { webauthn_challenge } + let(:webauthn_error) { nil } + let(:platform_authenticator) { false } + let(:client_data_json) { verification_client_data_json } + let!(:webauthn_configuration) do + return if !user + create( + :webauthn_configuration, + user: user, + credential_id: credential_id, + credential_public_key: credential_public_key, + platform_authenticator: platform_authenticator, + ) + end + + subject(:form) do + WebauthnVerificationForm.new( + user: user, + challenge: challenge, + protocol: protocol, + authenticator_data: authenticator_data, + client_data_json: client_data_json, + signature: signature, + credential_id: credential_id, + webauthn_error: webauthn_error, + ) + end describe '#submit' do before do - create( - :webauthn_configuration, - user: user, - credential_id: credential_id, - credential_public_key: credential_public_key, - platform_authenticator: platform_authenticator, - ) + allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') end - context 'when the input is valid for non-platform authenticator' do - it 'returns FormResponse with success: true' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') + subject(:result) { form.submit } - result = subject.submit( - protocol, - authenticator_data: authenticator_data, - client_data_json: verification_client_data_json, - signature: signature, - credential_id: credential_id, + context 'when the input is valid' do + it 'returns successful result' do + expect(result.to_h).to eq( + success: true, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, ) + end - expect(result.success?).to eq(true) - expect(result.to_h[:multi_factor_auth_method]).to eq('webauthn') + context 'for platform authenticator' do + let(:platform_authenticator) { true } + + it 'returns successful result' do + expect(result.to_h).to eq( + success: true, + multi_factor_auth_method: 'webauthn_platform', + webauthn_configuration_id: webauthn_configuration.id, + ) + end end end - context 'when the input is valid for platform authenticator' do - let(:platform_authenticator) { true } + context 'when the input is invalid' do + context 'when user is missing' do + let(:user) { nil } - it 'returns FormResponse with success: true' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { user: [:blank], webauthn_configuration: [:blank] }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: nil, + ) + end + end - result = subject.submit( - protocol, - authenticator_data: authenticator_data, - client_data_json: verification_client_data_json, - signature: signature, - credential_id: credential_id, - ) + context 'when challenge is missing' do + let(:challenge) { nil } - expect(result.success?).to eq(true) - expect(result.to_h[:multi_factor_auth_method]).to eq('webauthn_platform') + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + challenge: [:blank], + authenticator_data: [:invalid_authenticator_data], + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end end - end - context 'when the input is invalid' do - it 'returns FormResponse with success: false' do - result = subject.submit( - protocol, - authenticator_data: authenticator_data, - client_data_json: verification_client_data_json, - signature: signature, - credential_id: credential_id, - ) + context 'when authenticator data is missing' do + let(:authenticator_data) { nil } - expect(result.success?).to eq(false) - expect(result.to_h[:multi_factor_auth_method]).to eq('webauthn') + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + authenticator_data: [:blank, :invalid_authenticator_data], + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end end - it 'returns FormResponses with success: false when verification raises OpenSSL exception' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') - allow_any_instance_of(WebAuthn::AuthenticatorAssertionResponse).to receive(:verify). - and_raise(OpenSSL::PKey::PKeyError) - - result = subject.submit( - protocol, - authenticator_data: authenticator_data, - client_data_json: verification_client_data_json, - signature: signature, - credential_id: credential_id, - ) + context 'when client_data_json is missing' do + let(:client_data_json) { nil } + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + client_data_json: [:blank], + authenticator_data: [:invalid_authenticator_data], + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + end + + context 'when signature is missing' do + let(:signature) { nil } + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + signature: [:blank], + authenticator_data: [:invalid_authenticator_data], + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + end + + context 'when user has no configured webauthn' do + let(:webauthn_configuration) { nil } + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { webauthn_configuration: [:blank] }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: nil, + ) + end + end + + context 'when a client-side webauthn error is present' do + let(:webauthn_error) { 'Unexpected error!' } + + it 'returns unsuccessful result including client-side webauthn error text' do + expect(result.to_h).to eq( + success: false, + error_details: { webauthn_error: [webauthn_error] }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + end + + context 'when origin is invalid' do + before do + allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:6666') + end + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { authenticator_data: [:invalid_authenticator_data] }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + end + + context 'when verification raises OpenSSL exception' do + before do + allow_any_instance_of(WebAuthn::AuthenticatorAssertionResponse).to receive(:verify). + and_raise(OpenSSL::PKey::PKeyError) + end - expect(result.success?).to eq(false) - expect(result.to_h[:multi_factor_auth_method]).to eq('webauthn') + it 'returns unsucessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { authenticator_data: [:invalid_authenticator_data] }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end end end end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index f43a5898527..90a4f9a26a3 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -106,8 +106,7 @@ to_return(body: AamvaFixtures.verification_response) allow(IdentityConfig.store).to receive(:proofer_mock_fallback).and_return(false) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled). - and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) allow(IdentityConfig.store).to receive(:lexisnexis_account_id).and_return('abc123') allow(IdentityConfig.store).to receive(:lexisnexis_request_mode).and_return('aaa') @@ -299,8 +298,7 @@ allow(instance).to receive(:resolution_proofer).and_return(resolution_proofer) allow(instance).to receive(:state_id_proofer).and_return(state_id_proofer) allow(instance).to receive(:lexisnexis_ddp_proofer).and_return(ddp_proofer) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled). - and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) stub_request(:post, 'https://example.com/api/session-query'). with( body: hash_including('api_key' => 'test_api_key'), diff --git a/spec/jobs/threat_metrix_js_verification_job_spec.rb b/spec/jobs/threat_metrix_js_verification_job_spec.rb index 473d7aac72f..5390285ef46 100644 --- a/spec/jobs/threat_metrix_js_verification_job_spec.rb +++ b/spec/jobs/threat_metrix_js_verification_job_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe ThreatMetrixJsVerificationJob, type: :job do - let(:proofing_device_profiling_collecting_enabled) { true } + let(:threatmetrix_enabled) { true } let(:threatmetrix_org_id) { 'ABCD1234' } let(:threatmetrix_session_id) { 'some-session-id' } @@ -68,8 +68,8 @@ and_return(threatmetrix_org_id) allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_js_signing_cert). and_return(threatmetrix_signing_certificate) - allow(IdentityConfig.store).to receive(:proofing_device_profiling_collecting_enabled). - and_return(proofing_device_profiling_collecting_enabled) + allow(IdentityConfig.store).to receive(:proofing_device_profiling). + and_return(threatmetrix_enabled ? :collect_only : :disabled) stub_request(:get, "https://h.online-metrix.net/fp/tags.js?org_id=#{threatmetrix_org_id}&session_id=#{threatmetrix_session_id}"). to_return( diff --git a/spec/lib/data_requests/lookup_shared_device_users_spec.rb b/spec/lib/data_requests/lookup_shared_device_users_spec.rb index bfe20778940..92ac7d1fa76 100644 --- a/spec/lib/data_requests/lookup_shared_device_users_spec.rb +++ b/spec/lib/data_requests/lookup_shared_device_users_spec.rb @@ -21,7 +21,7 @@ result = subject.call - expect(result).to match_array([user1, user2, user3]) + expect(result).to match_array([user1, user2]) end end end diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb index bca45183128..03623eb4d56 100644 --- a/spec/lib/feature_management_spec.rb +++ b/spec/lib/feature_management_spec.rb @@ -366,4 +366,60 @@ expect(FeatureManagement.voip_allowed_phones).to eq(Set['+18885551234', '+18888675309']) end end + + describe '#proofing_device_profiling_collecting_enabled?' do + it 'returns false for disabled' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:disabled) + expect(FeatureManagement.proofing_device_profiling_collecting_enabled?).to eq(false) + end + it 'returns true for collect_only' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:collect_only) + expect(FeatureManagement.proofing_device_profiling_collecting_enabled?).to eq(true) + end + it 'returns true for enabled' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) + expect(FeatureManagement.proofing_device_profiling_collecting_enabled?).to eq(true) + end + it 'falls back to legacy config if needed' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(nil) + expect(IdentityConfig.store).to receive(:proofing_device_profiling_collecting_enabled). + twice. + and_return(true) + expect(FeatureManagement.proofing_device_profiling_collecting_enabled?).to eq(true) + end + it 'defaults to false' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(nil) + expect(IdentityConfig.store).to receive(:proofing_device_profiling_collecting_enabled). + and_return(nil) + expect(FeatureManagement.proofing_device_profiling_collecting_enabled?).to eq(false) + end + end + + describe '#proofing_device_profiling_decisioning_enabled?' do + it 'returns false for disabled' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:disabled) + expect(FeatureManagement.proofing_device_profiling_decisioning_enabled?).to eq(false) + end + it 'returns false for collect_only' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:collect_only) + expect(FeatureManagement.proofing_device_profiling_decisioning_enabled?).to eq(false) + end + it 'returns true for enabled' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) + expect(FeatureManagement.proofing_device_profiling_decisioning_enabled?).to eq(true) + end + it 'falls back to legacy config' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(nil) + expect(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). + twice. + and_return(true) + expect(FeatureManagement.proofing_device_profiling_decisioning_enabled?).to eq(true) + end + it 'defaults to false' do + expect(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(nil) + expect(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). + and_return(nil) + expect(FeatureManagement.proofing_device_profiling_decisioning_enabled?).to eq(false) + end + end end diff --git a/spec/services/idv/steps/verify_wait_step_show_spec.rb b/spec/services/idv/steps/verify_wait_step_show_spec.rb index fd515c62916..50131a64948 100644 --- a/spec/services/idv/steps/verify_wait_step_show_spec.rb +++ b/spec/services/idv/steps/verify_wait_step_show_spec.rb @@ -75,9 +75,7 @@ end it 'adds costs' do - allow(IdentityConfig.store).to receive(:proofing_device_profiling_collecting_enabled). - and_return(true) - allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) step.call diff --git a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb index a34818f893e..d44daf1b058 100644 --- a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb +++ b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb @@ -28,6 +28,11 @@ described_class.new(applicant: applicant, config: LexisNexisFixtures.example_ddp_config) end + before do + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_policy). + and_return('test-policy') + end + describe '#body' do it 'returns a properly formed request body' do expect(subject.body).to eq(LexisNexisFixtures.ddp_request_json) diff --git a/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb b/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb index a9e2e46a151..61304e75c64 100644 --- a/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb +++ b/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb @@ -60,7 +60,7 @@ expect(result.success?).to eq(false) expect(result.errors).to include( base: include(a_kind_of(String)), - 'Execute Instant Verify': include(a_kind_of(Hash)), + InstantVerify: include(a_kind_of(Hash)), ) expect(result.transaction_id).to eq('123456') expect(result.reference).to eq('0987:1234-abcd') diff --git a/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb b/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb index efda8a45649..7651741d8b4 100644 --- a/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb +++ b/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb @@ -65,7 +65,7 @@ expect(result.success?).to eq(false) expect(result.errors).to include( base: include(a_kind_of(String)), - 'PhoneFinder Checks': include(a_kind_of(Hash)), + PhoneFinder: include(a_kind_of(Hash)), ) expect(result.transaction_id).to eq('31000000000000') expect(result.reference).to eq('Reference1') diff --git a/spec/services/proofing/lexis_nexis/response_spec.rb b/spec/services/proofing/lexis_nexis/response_spec.rb index 751d9454244..f61d77c61c9 100644 --- a/spec/services/proofing/lexis_nexis/response_spec.rb +++ b/spec/services/proofing/lexis_nexis/response_spec.rb @@ -33,7 +33,7 @@ errors = subject.verification_errors expect(errors).to be_a(Hash) - expect(errors).to include(:base, :'Execute Instant Verify') + expect(errors).to include(:base, :InstantVerify) end end @@ -72,7 +72,7 @@ errors = subject.verification_errors expect(errors).to be_a(Hash) - expect(errors).to include(:base, :'Execute Instant Verify') + expect(errors).to include(:base, :InstantVerify) expect(errors[:base]).to eq("Invalid status in response body: 'fake_status'") end end diff --git a/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb b/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb index 063e40b0941..36c6ced309a 100644 --- a/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb +++ b/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb @@ -6,21 +6,12 @@ end subject(:error_parser) { described_class.new(response_body) } - describe '#initialize' do - let(:response_body) do - JSON.parse(LexisNexisFixtures.instant_verify_date_of_birth_fail_response_json) - end - end - describe '#parsed_errors' do subject(:errors) { error_parser.parsed_errors } it 'should return an array of errors from the response' do expect(errors[:base]).to start_with('Verification failed with code:') - expect(errors[:Discovery]).to eq(nil) # This should be absent since it passed - expect(errors[:SomeOtherProduct]).to eq(response_body['Products'][1]) - expect(errors[:InstantVerify]).to eq(response_body['Products'][2]) - expect(errors[:'Execute Instant Verify']).to be_a Hash # log product error + expect(errors[:InstantVerify]).to eq(response_body['Products'].first) # log product error end it 'should not log a passing response containing no important information' do @@ -28,21 +19,21 @@ response_body['Products'].first['ProductStatus'] = 'pass' response_body['Products'].first['Items'].map { |i| i.delete('ItemReason') } - expect(errors[:'Execute Instant Verify']).to eq(nil) + expect(errors[:'Fake Product']).to eq(nil) end it 'should log any Instant Verify response, including a pass' do response_body['Products'].first['ProductStatus'] = 'pass' response_body['Products'].first['Items'].map { |i| i.delete('ItemReason') } - expect(errors[:'Execute Instant Verify']).to be_a Hash + expect(errors[:InstantVerify]).to be_a Hash end it 'should log any response with an ItemReason, including a pass' do response_body['Products'].first['ProductType'] = 'Fake Product' response_body['Products'].first['ProductStatus'] = 'pass' - expect(errors[:'Execute Instant Verify']).to be_a Hash + expect(errors[:'Fake Product']).to be_a Hash end end end diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 61e56c2dcbd..0973f2a1993 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -108,6 +108,12 @@ def saml_get_auth(settings) def saml_post_auth(saml_request) # POST redirect binding Authn Request + request.path = '/api/saml/authpost2022' + post :auth, params: { SAMLRequest: CGI.unescape(saml_request) } + end + + def saml_final_post_auth(saml_request) + request.path = '/api/saml/finalauthpost2022' post :auth, params: { SAMLRequest: CGI.unescape(saml_request) } end diff --git a/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb b/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb index c254a1bf397..32431c94380 100644 --- a/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb +++ b/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb @@ -46,6 +46,15 @@ end end + it 'renders a cancel link' do + render + + expect(rendered).to have_link( + t('in_person_proofing.body.barcode.cancel_link_text'), + href: idv_cancel_path(step: 'verify'), + ) + end + context 'with enrollment where current address matches id' do let(:current_address_matches_id) { true } diff --git a/spec/views/idv/shared/_ssn.html.erb_spec.rb b/spec/views/idv/shared/_ssn.html.erb_spec.rb index 25829b40b13..112e5af8ba9 100644 --- a/spec/views/idv/shared/_ssn.html.erb_spec.rb +++ b/spec/views/idv/shared/_ssn.html.erb_spec.rb @@ -3,7 +3,7 @@ describe 'idv/shared/_ssn.html.erb' do include Devise::Test::ControllerHelpers - let(:proofing_device_profiling_collecting_enabled) { nil } + let(:threatmetrix_enabled) { nil } let(:lexisnexis_threatmetrix_org_id) { 'test_org_id' } let(:session_id) { 'ABCD-1234' } let(:updating_ssn) { false } @@ -20,9 +20,8 @@ before :each do allow(view).to receive(:url_for).and_return('https://example.com/') - allow(IdentityConfig.store). - to receive(:proofing_device_profiling_collecting_enabled). - and_return(proofing_device_profiling_collecting_enabled) + allow(IdentityConfig.store).to receive(:proofing_device_profiling). + and_return(threatmetrix_enabled ? :enabled : :disabled) allow(IdentityConfig.store). to receive(:lexisnexis_threatmetrix_org_id).and_return(lexisnexis_threatmetrix_org_id) @@ -37,49 +36,36 @@ end context 'when threatmetrix collection enabled' do - let(:proofing_device_profiling_collecting_enabled) { true } - - context 'and org id specified' do - context 'and entering ssn for the first time' do - describe '