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 '