diff --git a/.mocharc.js b/.mocharc.js
index 6a156b6e61d..1806a14ce37 100644
--- a/.mocharc.js
+++ b/.mocharc.js
@@ -4,6 +4,5 @@ module.exports = /** @type {import('mocha').MochaOptions} */ ({
require: ['./spec/javascript/support/mocha.js'],
file: 'spec/javascript/spec_helper.js',
extension: ['js', 'jsx', 'ts', 'tsx'],
- loader: ['quibble'],
conditions: ['source'],
});
diff --git a/Gemfile.lock b/Gemfile.lock
index 2cb32e99b65..eea82ac68fa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -321,8 +321,8 @@ GEM
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
- faker (2.19.0)
- i18n (>= 1.6, < 2)
+ faker (3.4.2)
+ i18n (>= 1.8.11, < 2)
faraday (2.10.0)
faraday-net_http (>= 2.0, < 3.2)
logger
diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb
index 3a4b052d757..64ced91aba2 100644
--- a/app/controllers/idv/agreement_controller.rb
+++ b/app/controllers/idv/agreement_controller.rb
@@ -36,7 +36,6 @@ def update
)
if result.success?
- idv_session.idv_consent_given = true
idv_session.idv_consent_given_at = Time.zone.now
if IdentityConfig.store.in_person_proofing_opt_in_enabled &&
diff --git a/app/controllers/idv/in_person/state_id_controller.rb b/app/controllers/idv/in_person/state_id_controller.rb
index ea073a12fd0..ad9bc723cee 100644
--- a/app/controllers/idv/in_person/state_id_controller.rb
+++ b/app/controllers/idv/in_person/state_id_controller.rb
@@ -6,7 +6,6 @@ class StateIdController < ApplicationController
include Idv::AvailabilityConcern
include IdvStepConcern
- before_action :render_404_if_controller_not_enabled
before_action :redirect_unless_enrollment # confirm previous step is complete
before_action :set_usps_form_presenter
@@ -92,11 +91,6 @@ def self.step_info
private
- def render_404_if_controller_not_enabled
- render_not_found unless
- IdentityConfig.store.in_person_state_id_controller_enabled
- end
-
def redirect_unless_enrollment
redirect_to idv_document_capture_url unless current_user.establishing_in_person_enrollment
end
diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb
index d4f9d4cdbfa..5284d5e6881 100644
--- a/app/controllers/idv/in_person/verify_info_controller.rb
+++ b/app/controllers/idv/in_person/verify_info_controller.rb
@@ -75,7 +75,10 @@ def prev_url
def pii
pii_from_user = user_session.dig('idv/in_person', :pii_from_user) || {}
- pii_from_user.merge(ssn: idv_session.ssn)
+ pii_from_user.merge(
+ consent_given_at: idv_session.idv_consent_given_at,
+ ssn: idv_session.ssn,
+ )
end
# override IdvSessionConcern
diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb
index c69563bb272..16d0a51d01f 100644
--- a/app/controllers/idv/verify_info_controller.rb
+++ b/app/controllers/idv/verify_info_controller.rb
@@ -79,6 +79,7 @@ def analytics_arguments
def pii
idv_session.pii_from_doc.to_h.merge(
ssn: idv_session.ssn,
+ consent_given_at: idv_session.idv_consent_given_at,
**idv_session.updated_user_address.to_h,
).with_indifferent_access
end
diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb
index 362106b8e35..012dd21a40e 100644
--- a/app/controllers/saml_idp_controller.rb
+++ b/app/controllers/saml_idp_controller.rb
@@ -129,7 +129,7 @@ def capture_analytics
finish_profile: user_has_pending_profile?,
requested_ial: requested_ial,
request_signed: saml_request.signed?,
- matching_cert_serial: saml_request.service_provider.matching_cert&.serial&.to_s,
+ matching_cert_serial:,
requested_nameid_format: saml_request.name_id_format,
)
@@ -140,6 +140,12 @@ def capture_analytics
analytics.saml_auth(**analytics_payload)
end
+ def matching_cert_serial
+ saml_request.matching_cert&.serial&.to_s
+ rescue SamlIdp::XMLSecurity::SignedDocument::ValidationError
+ nil
+ end
+
def log_external_saml_auth_request
return unless external_saml_request?
diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
index 20b912feda6..c58a1967315 100644
--- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
@@ -7,7 +7,6 @@ class WebauthnVerificationController < ApplicationController
include NewDeviceConcern
before_action :check_sp_required_mfa
- before_action :check_if_device_supports_platform_auth, only: :show
before_action :confirm_webauthn_enabled, only: :show
def show
@@ -23,17 +22,6 @@ def confirm
private
- def check_if_device_supports_platform_auth
- return unless user_session.has_key?(:platform_authenticator_available)
- if platform_authenticator? && !device_supports_webauthn_platform?
- redirect_to login_two_factor_options_url
- end
- end
-
- def device_supports_webauthn_platform?
- user_session.delete(:platform_authenticator_available) == true
- end
-
def handle_webauthn_result(result)
handle_verification_for_authentication_context(
result:,
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
index b56ccf6524b..57a5444489e 100644
--- a/app/controllers/users/two_factor_authentication_controller.rb
+++ b/app/controllers/users/two_factor_authentication_controller.rb
@@ -338,17 +338,19 @@ def otp_rate_limiter
def redirect_url
if !mobile? && TwoFactorAuthentication::PivCacPolicy.new(current_user).enabled?
login_two_factor_piv_cac_url
+ elsif TwoFactorAuthentication::WebauthnPolicy.new(current_user).platform_enabled?
+ if user_session[:platform_authenticator_available] == false
+ login_two_factor_options_url
+ else
+ login_two_factor_webauthn_url(platform: true)
+ end
elsif TwoFactorAuthentication::WebauthnPolicy.new(current_user).enabled?
- login_two_factor_webauthn_url(webauthn_params)
+ login_two_factor_webauthn_url
elsif TwoFactorAuthentication::AuthAppPolicy.new(current_user).enabled?
login_two_factor_authenticator_url
end
end
- def webauthn_params
- { platform: current_user.webauthn_configurations.platform_authenticators.present? }
- end
-
def handle_too_many_short_term_otp_sends(method:, default:)
analytics.rate_limit_reached(
limiter_type: short_term_otp_rate_limiter.rate_limit_type,
diff --git a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx
similarity index 62%
rename from spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx
rename to app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx
index 729e574c145..cc37fdf45aa 100644
--- a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx
+++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx
@@ -2,6 +2,8 @@ import { render, screen, within } from '@testing-library/react';
import DocumentCaptureReviewIssues from '@18f/identity-document-capture/components/document-capture-review-issues';
import { InPersonContext } from '@18f/identity-document-capture/context';
import { toFormEntryError } from '@18f/identity-document-capture/services/upload';
+import { I18nContext } from '@18f/identity-react-i18n';
+import { I18n } from '@18f/identity-i18n';
import { expect } from 'chai';
import { composeComponents } from '@18f/identity-compose-components';
@@ -9,19 +11,48 @@ describe('DocumentCaptureReviewIssues', () => {
const DEFAULT_OPTIONS = {
registerField: () => undefined,
captureHints: true,
- remainingAttempts: 2,
value: {},
onChange: () => undefined,
onError: () => undefined,
+ isFailedSelfie: false,
+ isFailedDocType: false,
+ isFailedSelfieLivenessOrQuality: false,
+ remainingSubmitAttempts: Infinity,
+ unknownFieldErrors: [],
+ errors: [],
+ hasDismissed: false,
+ toPreviousStep: () => undefined,
};
+
+ context('with default props', () => {
+ it('does not display infinity remaining attempts', () => {
+ const { queryByText } = render(
+
+
+ ,
+ );
+
+ expect(queryByText('You have Infinity attempts remaining.')).to.equal(null);
+ });
+ });
+
context('with doc error', () => {
it('renders for non doc type failure', () => {
const props = {
isFailedDocType: false,
+ remainingSubmitAttempts: 2,
unknownFieldErrors: [
{
field: 'general',
- error: toFormEntryError({ field: 'general', message: 'general error' }),
+ error: toFormEntryError({ field: 'network', message: 'general error' }),
},
],
errors: [
@@ -44,6 +75,16 @@ describe('DocumentCaptureReviewIssues', () => {
},
},
],
+ [
+ I18nContext.Provider,
+ {
+ value: new I18n({
+ strings: {
+ 'idv.failure.attempts_html': 'You have %{count} attempts remaining.',
+ },
+ }),
+ },
+ ],
[
DocumentCaptureReviewIssues,
{
@@ -58,6 +99,8 @@ describe('DocumentCaptureReviewIssues', () => {
expect(getByText('general error')).to.be.ok();
+ expect(getByText('You have 2 attempts remaining.')).to.be.ok();
+
// tips header
expect(getByText('doc_auth.tips.review_issues_id_header_text')).to.be.ok();
const lists = getAllByRole('list');
@@ -80,31 +123,37 @@ describe('DocumentCaptureReviewIssues', () => {
});
it('renders for a doc type failure', () => {
- const props = {
- isFailedDocType: true,
- unknownFieldErrors: [
- {
- field: 'general',
- error: toFormEntryError({ field: 'general', message: 'general error' }),
- },
- ],
- errors: [
- {
- field: 'front',
- error: toFormEntryError({ field: 'front', message: 'front side doc type error' }),
- },
- {
- field: 'back',
- error: toFormEntryError({ field: 'back', message: 'back side doc type error' }),
- },
- ],
- };
const { getByText, getByLabelText, getByRole } = render(
-
+
,
diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
index 221b5f88fa5..2b7366b0b5a 100644
--- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
@@ -60,11 +60,13 @@ function DocumentCaptureReviewIssues({
altFailedDocTypeMsg={isFailedDocType ? t('doc_auth.errors.doc.wrong_id_type_html') : null}
hasDismissed={hasDismissed}
/>
-
-
-
+ {Number.isFinite(remainingSubmitAttempts) && (
+
+
+
+ )}
{!isFailedDocType && captureHints && (
{
user,
challenge,
excludeCredentials,
- authenticatorAttachment: 'cross-platform',
- hints: ['security-key'],
});
expect(navigator.credentials.create).to.have.been.calledWith({
@@ -130,16 +128,15 @@ describe('enrollWebauthnDevice', () => {
context('platform authenticator', () => {
it('enrolls a device with correct authenticatorAttachment', async () => {
await enrollWebauthnDevice({
+ platformAuthenticator: true,
user,
challenge,
excludeCredentials,
- authenticatorAttachment: 'platform',
- hints: ['client-device'],
});
expect(navigator.credentials.create).to.have.been.calledWithMatch({
publicKey: {
- hints: ['client-device'],
+ hints: undefined,
authenticatorSelection: {
authenticatorAttachment: 'platform',
},
@@ -162,7 +159,6 @@ describe('enrollWebauthnDevice', () => {
user,
challenge,
excludeCredentials,
- authenticatorAttachment: 'cross-platform',
});
expect(result.transports).to.equal(undefined);
@@ -182,7 +178,6 @@ describe('enrollWebauthnDevice', () => {
user,
challenge,
excludeCredentials,
- authenticatorAttachment: 'cross-platform',
});
expect(result.authenticatorDataFlagsValue).to.equal(undefined);
diff --git a/app/javascript/packages/webauthn/enroll-webauthn-device.ts b/app/javascript/packages/webauthn/enroll-webauthn-device.ts
index bf19d2a75db..f853488173e 100644
--- a/app/javascript/packages/webauthn/enroll-webauthn-device.ts
+++ b/app/javascript/packages/webauthn/enroll-webauthn-device.ts
@@ -18,15 +18,13 @@ interface AuthenticatorAttestationResponseBrowserSupport
type PublicKeyCredentialHintType = 'client-device' | 'security-key' | 'hybrid';
interface EnrollOptions {
+ platformAuthenticator?: boolean;
+
user: PublicKeyCredentialUserEntity;
challenge: BufferSource;
excludeCredentials: PublicKeyCredentialDescriptor[];
-
- authenticatorAttachment?: AuthenticatorAttachment;
-
- hints?: Array;
}
interface EnrollResult {
@@ -80,11 +78,10 @@ const SUPPORTED_ALGORITHMS: COSEAlgorithm[] = [
];
async function enrollWebauthnDevice({
+ platformAuthenticator = false,
user,
challenge,
excludeCredentials,
- authenticatorAttachment,
- hints,
}: EnrollOptions): Promise {
const credential = (await navigator.credentials.create({
publicKey: {
@@ -94,11 +91,16 @@ async function enrollWebauthnDevice({
pubKeyCredParams: SUPPORTED_ALGORITHMS.map((alg) => ({ alg, type: 'public-key' })),
timeout: 800000,
attestation: 'none',
- hints,
+ hints: platformAuthenticator ? undefined : ['security-key'],
authenticatorSelection: {
- // Prevents user from needing to use PIN with Security Key
+ // A user is assumed to be AAL2 recently authenticated before being permitted to add an
+ // authentication method to their account. Additionally, unless explicitly discouraged,
+ // Windows devices will prompt the user to add a PIN to their security key. When used as a
+ // single-factor authenticator in combination with a memorized secret (password), proving
+ // possession is sufficient, and a PIN ("something you know") is unnecessary friction that
+ // contributes to abandonment or loss of access.
userVerification: 'discouraged',
- authenticatorAttachment,
+ authenticatorAttachment: platformAuthenticator ? 'platform' : 'cross-platform',
},
excludeCredentials,
} as PublicKeyCredentialCreationOptionsWithHints,
diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts
index 743b99c827c..b0c2d0c9eff 100644
--- a/app/javascript/packs/webauthn-setup.ts
+++ b/app/javascript/packs/webauthn-setup.ts
@@ -42,6 +42,7 @@ function webauthn() {
(document.getElementById('platform_authenticator') as HTMLInputElement).value === 'true';
enrollWebauthnDevice({
+ platformAuthenticator,
user: {
id: longToByteArray(Number((document.getElementById('user_id') as HTMLInputElement).value)),
name: (document.getElementById('user_email') as HTMLInputElement).value,
@@ -55,8 +56,6 @@ function webauthn() {
.split(',')
.filter(Boolean),
),
- authenticatorAttachment: platformAuthenticator ? 'platform' : 'cross-platform',
- hints: platformAuthenticator ? ['client-device', 'hybrid'] : ['security-key'],
})
.then((result) => {
(document.getElementById('webauthn_id') as HTMLInputElement).value = result.webauthnId;
diff --git a/app/jobs/reports/mfa_report.rb b/app/jobs/reports/mfa_report.rb
new file mode 100644
index 00000000000..af416981a67
--- /dev/null
+++ b/app/jobs/reports/mfa_report.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'reporting/mfa_report'
+
+module Reports
+ class MfaReport < BaseReport
+ REPORT_NAME = 'mfa-report'
+
+ attr_accessor :report_date
+
+ def perform(report_date)
+ return unless IdentityConfig.store.s3_reports_enabled
+
+ self.report_date = report_date
+ message = "Report: #{REPORT_NAME} #{report_date}"
+ subject = "Monthly MFA Report - #{report_date}"
+
+ report_configs.each do |report_hash|
+ reports = monthly_mfa_emailable_reports(report_hash['issuers'])
+
+ report_hash['emails'].each do |email|
+ ReportMailer.tables_report(
+ email:,
+ subject:,
+ message:,
+ reports:,
+ attachment_format: :csv,
+ ).deliver_now
+ end
+ end
+ end
+
+ private
+
+ def monthly_mfa_emailable_reports(issuers)
+ Reporting::MfaReport.new(
+ issuers:,
+ time_range: report_date.all_month,
+ ).as_emailable_reports
+ end
+
+ def report_configs
+ IdentityConfig.store.mfa_report_config
+ end
+ end
+end
diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb
index fc39a3280c2..399f8440f22 100644
--- a/app/jobs/socure_shadow_mode_proofing_job.rb
+++ b/app/jobs/socure_shadow_mode_proofing_job.rb
@@ -100,6 +100,7 @@ def build_applicant(
:phone,
:dob,
:ssn,
+ :consent_given_at,
),
email: user_email,
}
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index f01ca2b86a9..da60ab14e2c 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -1925,9 +1925,9 @@ def idv_doc_auth_verify_polling_wait_visited(**extra)
# @option proofing_results [String,nil] context.stages.threatmetrix.exception If an exception was encountered making the request to the vendor, its message is provided here.
# @option proofing_results [Boolean] context.stages.threatmetrix.timed_out Whether the request to the vendor timed out.
# @option proofing_results [String] context.stages.threatmetrix.transaction_id Vendor-specific transaction ID for the request.
+ # @option proofing_results [String] context.stages.threatmetrix.session_id Session ID associated with the response.
+ # @option proofing_results [String] context.stages.threatmetrix.account_lex_id LexID associated with the response.
# @option proofing_results [Hash] context.stages.threatmetrix.response_body JSON body of the response returned from the vendor. PII has been redacted.
- # @option proofing_results [String] context.stages.threatmetrix.response_body.account_lex_id LexID associated with the response.
- # @option proofing_results [String] context.stages.threatmetrix.response_body.session_id Session ID associated with the response.
# @option proofing_results [String] context.stages.threatmetrix.review_status One of "pass", "review", "reject".
# @param skip_hybrid_handoff [Boolean] Whether the user should skip hybrid handoff (i.e. because they are already on a mobile device)
# @param ssn_is_unique [Boolean] Whether another Profile existed with the same SSN at the time the profile associated with the current IdV session was minted.
diff --git a/app/services/proofing/ddp_result.rb b/app/services/proofing/ddp_result.rb
index 9670d570078..7c5dddc3ebd 100644
--- a/app/services/proofing/ddp_result.rb
+++ b/app/services/proofing/ddp_result.rb
@@ -7,6 +7,8 @@ class DdpResult
:success,
:transaction_id,
:review_status,
+ :account_lex_id,
+ :session_id,
:response_body,
:client
@@ -16,6 +18,8 @@ def initialize(
context: {},
exception: nil,
transaction_id: nil,
+ account_lex_id: nil,
+ session_id: nil,
review_status: nil,
response_body: nil,
client: nil
@@ -25,6 +29,8 @@ def initialize(
@context = context
@exception = exception
@transaction_id = transaction_id
+ @account_lex_id = account_lex_id
+ @session_id = session_id
@response_body = response_body
@review_status = review_status
@client = client
@@ -70,6 +76,8 @@ def to_h
timed_out: timed_out?,
transaction_id: transaction_id,
review_status: review_status,
+ account_lex_id: account_lex_id,
+ session_id: session_id,
response_body: redacted_response_body,
}
end
diff --git a/app/services/proofing/lexis_nexis/ddp/proofer.rb b/app/services/proofing/lexis_nexis/ddp/proofer.rb
index 6b7d296c312..e1a7da5f904 100644
--- a/app/services/proofing/lexis_nexis/ddp/proofer.rb
+++ b/app/services/proofing/lexis_nexis/ddp/proofer.rb
@@ -36,6 +36,8 @@ def build_result_from_response(verification_response)
result.review_status = review_status
result.add_error(:request_result, request_result) unless request_result == 'success'
result.add_error(:review_status, review_status) unless review_status == 'pass'
+ result.account_lex_id = body['account_lex_id']
+ result.session_id = body['session_id']
result.success = !result.errors?
result.client = 'lexisnexis'
diff --git a/app/services/proofing/mock/ddp_mock_client.rb b/app/services/proofing/mock/ddp_mock_client.rb
index 772443a48b6..a0118e5e60c 100644
--- a/app/services/proofing/mock/ddp_mock_client.rb
+++ b/app/services/proofing/mock/ddp_mock_client.rb
@@ -39,6 +39,8 @@ def stage
'ddp',
).freeze
TRANSACTION_ID = 'ddp-mock-transaction-id-123'
+ ACCOUNT_LEX_ID = 'super-cool-test-lex-id'
+ SESSION_ID = 'super-cool-test-session-id'
def initialize(response_fixture_file: 'successful_response.json')
@response_fixture_file = File.expand_path(response_fixture_file, FIXTURES_DIR)
@@ -55,6 +57,8 @@ def proof(applicant)
return exception_result if review_status.nil?
result.review_status = review_status
+ result.account_lex_id = ACCOUNT_LEX_ID
+ result.session_id = SESSION_ID
result.response_body = response_body
result.add_error(:review_status, review_status) unless review_status == 'pass'
diff --git a/app/services/proofing/socure/id_plus/input.rb b/app/services/proofing/socure/id_plus/input.rb
index 28070fc51f2..7fd095ee36e 100644
--- a/app/services/proofing/socure/id_plus/input.rb
+++ b/app/services/proofing/socure/id_plus/input.rb
@@ -16,7 +16,9 @@ module IdPlus
:phone,
:email,
:ssn,
+ :consent_given_at,
keyword_init: true,
+ allowed_members: [:consent_given_at],
).freeze
end
end
diff --git a/app/services/proofing/socure/id_plus/request.rb b/app/services/proofing/socure/id_plus/request.rb
index d15591eb2aa..881c495d63d 100644
--- a/app/services/proofing/socure/id_plus/request.rb
+++ b/app/services/proofing/socure/id_plus/request.rb
@@ -98,7 +98,7 @@ def body
dob: input.dob&.to_date&.to_s,
userConsent: true,
- consentTimestamp: 5.minutes.ago.iso8601,
+ consentTimestamp: input.consent_given_at&.to_time&.iso8601,
email: input.email,
mobileNumber: input.phone,
diff --git a/config/application.yml.default b/config/application.yml.default
index 4cece14a740..63d0fd90c9b 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -228,6 +228,7 @@ max_mail_events: 4
max_mail_events_window_in_days: 30
max_phone_numbers_per_account: 5
max_piv_cac_per_account: 2
+mfa_report_config: '[]'
min_password_score: 3
minimum_wait_before_another_usps_letter_in_hours: 24
mx_timeout: 3
diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb
index 9d74b63cecd..ac97907c86e 100644
--- a/config/initializers/job_configurations.rb
+++ b/config/initializers/job_configurations.rb
@@ -9,6 +9,7 @@
cron_every_monday = 'every Monday at 0:25 UTC' # equivalent to '25 0 * * 1'
cron_every_monday_1am = 'every Monday at 1:00 UTC' # equivalent to '0 1 * * 1'
cron_every_monday_2am = 'every Monday at 2:00 UTC' # equivalent to '0 2 * * 1'
+cron_monthly = '30 0 1 * *' # monthly, 0:30 UTC to not overlap with jobs running at 0000
if defined?(Rails::Console)
Rails.logger.info 'job_configurations: console detected, skipping schedule'
@@ -217,7 +218,7 @@
cron: cron_24h_and_a_bit,
args: -> { [Time.zone.yesterday.end_of_day] },
},
- # Previous week's drop of report
+ # Previous week's drop off report
weekly_drop_off_report: {
class: 'Reports::DropOffReport',
cron: cron_every_monday_1am,
@@ -229,6 +230,12 @@
cron: cron_every_monday_2am,
args: -> { [Time.zone.yesterday.end_of_day] },
},
+ # Previous months's mfa report
+ monthly_mfa_report: {
+ class: 'Reports::MfaReport',
+ cron: cron_monthly,
+ args: -> { [Time.zone.yesterday.end_of_day] },
+ },
}.compact
end
# rubocop:enable Metrics/BlockLength
diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml
index 362e0111672..9b905e8ad0f 100644
--- a/config/service_providers.localdev.yml
+++ b/config/service_providers.localdev.yml
@@ -450,6 +450,21 @@ development:
friendly_name: 'Example Sinatra App'
in_person_proofing_enabled: true
+ 'urn:gov:gsa:openidconnect:sp:sinatra_pkce':
+ agency_id: 1
+ ial: 2
+ push_notification_url: http://localhost:9292/api/push_notifications
+ return_to_sp_url: 'http://localhost:9292'
+ redirect_uris:
+ - 'http://localhost:9292/'
+ - 'http://localhost:9292/auth/result'
+ - 'http://localhost:9292/logout'
+ certs:
+ - 'sp_sinatra_demo'
+ friendly_name: 'Example Sinatra App with PKCE'
+ in_person_proofing_enabled: true
+ pkce: true
+
'urn:gov:gsa:openidconnect:sp:expressjs':
agency: 'GSA'
certs:
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 8f9c0909de1..3dd9c8f1f98 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -247,6 +247,7 @@ def self.store
config.add(:max_mail_events_window_in_days, type: :integer)
config.add(:max_phone_numbers_per_account, type: :integer)
config.add(:max_piv_cac_per_account, type: :integer)
+ config.add(:mfa_report_config, type: :json)
config.add(:min_password_score, type: :integer)
config.add(:minimum_wait_before_another_usps_letter_in_hours, type: :integer)
config.add(:mx_timeout, type: :integer)
diff --git a/scripts/enforce-typescript-files.mjs b/scripts/enforce-typescript-files.mjs
index f77f6d396a0..90341fe763f 100755
--- a/scripts/enforce-typescript-files.mjs
+++ b/scripts/enforce-typescript-files.mjs
@@ -44,7 +44,6 @@ const LEGACY_FILE_EXCEPTIONS = [
'spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx',
'spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx',
'spec/javascript/packages/document-capture/components/callback-on-mount-spec.jsx',
- 'spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx',
'spec/javascript/packages/document-capture/components/document-capture-spec.jsx',
'spec/javascript/packages/document-capture/components/document-capture-warning-spec.jsx',
'spec/javascript/packages/document-capture/components/file-image-spec.jsx',
diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb
index 1b195113fca..f977cdf27fc 100644
--- a/spec/controllers/idv/in_person/state_id_controller_spec.rb
+++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb
@@ -28,32 +28,6 @@
)
end
- context '#render_404_if_controller_not_enabled' do
- context 'flag not set' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(nil)
- end
- it 'renders a 404' do
- get :show
-
- expect(response).to be_not_found
- end
- end
-
- context 'flag not enabled' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(false)
- end
- it 'renders a 404' do
- get :show
-
- expect(response).to be_not_found
- end
- end
- end
-
context '#confirm_establishing_enrollment' do
let(:enrollment) { nil }
it 'redirects to document capture if not complete' do
diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb
index ebea673e80c..22cb5cbe811 100644
--- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb
@@ -13,6 +13,7 @@
stub_sign_in(user)
subject.idv_session.flow_path = 'standard'
subject.idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID[:ssn]
+ subject.idv_session.idv_consent_given_at = Time.zone.now.to_s
subject.user_session['idv/in_person'] = flow_session
end
@@ -216,6 +217,7 @@
hash_including(
state_id_type: 'drivers_license',
ssn: Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID[:ssn],
+ consent_given_at: subject.idv_session.idv_consent_given_at,
),
).and_call_original
@@ -264,7 +266,9 @@
it 'captures state id address fields in the pii' do
expect(Idv::Agent).to receive(:new).with(
- Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS,
+ Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS.merge(
+ consent_given_at: subject.idv_session.idv_consent_given_at,
+ ),
).and_call_original
put :update
end
diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb
index 67107ec790e..1be8cdbe1d0 100644
--- a/spec/controllers/idv/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/verify_info_controller_spec.rb
@@ -397,6 +397,7 @@
expect(Idv::Agent).to receive(:new).with(
hash_including(
ssn: Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn],
+ consent_given_at: controller.idv_session.idv_consent_given_at,
**Idp::Constants::MOCK_IDV_APPLICANT,
),
).and_call_original
diff --git a/spec/controllers/users/email_confirmations_controller_spec.rb b/spec/controllers/users/email_confirmations_controller_spec.rb
index b6495043c2b..8c984d65584 100644
--- a/spec/controllers/users/email_confirmations_controller_spec.rb
+++ b/spec/controllers/users/email_confirmations_controller_spec.rb
@@ -5,7 +5,7 @@
describe 'Valid email confirmation tokens' do
it 'tracks a valid email confirmation token event' do
user = create(:user)
- new_email = Faker::Internet.safe_email
+ new_email = Faker::Internet.email
expect(PushNotification::HttpPush).to receive(:deliver).once.
with(PushNotification::EmailChangedEvent.new(
@@ -27,7 +27,7 @@
it 'rejects an otherwise valid token for unconfirmed users' do
user = create(:user, :unconfirmed, email_addresses: [])
- new_email = Faker::Internet.safe_email
+ new_email = Faker::Internet.email
add_email_form = AddUserEmailForm.new
add_email_form.submit(user, email: new_email)
@@ -41,7 +41,7 @@
it 'rejects expired tokens' do
user = create(:user)
- new_email = Faker::Internet.safe_email
+ new_email = Faker::Internet.email
add_email_form = AddUserEmailForm.new
add_email_form.submit(user, email: new_email)
diff --git a/spec/controllers/users/emails_controller_spec.rb b/spec/controllers/users/emails_controller_spec.rb
index 05bb2201d48..2bc857706c5 100644
--- a/spec/controllers/users/emails_controller_spec.rb
+++ b/spec/controllers/users/emails_controller_spec.rb
@@ -30,7 +30,7 @@
stub_sign_in(user)
while EmailPolicy.new(user).can_add_email?
- email = Faker::Internet.safe_email
+ email = Faker::Internet.email
user.email_addresses.create(email: email, confirmed_at: Time.zone.now)
end
end
@@ -53,7 +53,7 @@
context 'valid email exists in session' do
it 'sends email' do
- email = Faker::Internet.safe_email
+ email = Faker::Internet.email
post :add, params: { user: { email: email } }
diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb
index 6d1588d5bcb..be407001213 100644
--- a/spec/controllers/users/sessions_controller_spec.rb
+++ b/spec/controllers/users/sessions_controller_spec.rb
@@ -906,7 +906,7 @@
render_views
it 'does not prefill the form' do
- email = Faker::Internet.safe_email
+ email = Faker::Internet.email
password = SecureRandom.uuid
get :new, params: { user: { email: email, password: password } }
diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb
index 0c1cd63d7ab..2b4eeb055ce 100644
--- a/spec/controllers/users/two_factor_authentication_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb
@@ -176,28 +176,39 @@ def index
end
end
- context 'when user is webauthn enabled' do
+ context 'when user has webauthn' do
+ let(:user) { create(:user, :with_webauthn) }
+
before do
- stub_sign_in_before_2fa(create(:user, :with_webauthn))
+ stub_sign_in_before_2fa(user)
end
- it 'renders the :webauthn view' do
+ it 'redirects to webauthn verification' do
get :show
- expect(response).to redirect_to login_two_factor_webauthn_path(platform: false)
+ expect(response).to redirect_to login_two_factor_webauthn_path
end
- context 'when platform_authenticator' do
- before do
- controller.current_user.webauthn_configurations.
- first.update!(platform_authenticator: true)
- end
+ context 'when user has platform webauthn' do
+ let(:user) { create(:user, :with_webauthn_platform) }
- it 'passes the platform parameter if the user has a platform autheticator' do
+ it 'redirects to webauthn verification with the platform parameter' do
get :show
expect(response).to redirect_to login_two_factor_webauthn_path(platform: true)
end
+
+ context 'when session value indicates no device platform support available' do
+ before do
+ controller.user_session[:platform_authenticator_available] = false
+ end
+
+ it 'redirects to mfa options page' do
+ get :show
+
+ expect(response).to redirect_to login_two_factor_options_path
+ end
+ end
end
end
diff --git a/spec/factories/email_addresses.rb b/spec/factories/email_addresses.rb
index 620d7b04cd4..975f2e8b883 100644
--- a/spec/factories/email_addresses.rb
+++ b/spec/factories/email_addresses.rb
@@ -4,7 +4,7 @@
factory :email_address do
confirmed_at { Time.zone.now }
confirmation_sent_at { Time.zone.now - 5.minutes }
- email { Faker::Internet.safe_email }
+ email { Faker::Internet.email }
user { association :user }
trait :unconfirmed do
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 4659097e929..4e0cec5aadd 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -47,7 +47,7 @@
until user.email_addresses.many?
user.email_addresses << build(
:email_address,
- email: Faker::Internet.safe_email,
+ email: Faker::Internet.email,
confirmed_at: user.confirmed_at,
user_id: -1,
)
@@ -57,7 +57,7 @@
until user.email_addresses.many?
user.email_addresses << build(
:email_address,
- email: Faker::Internet.safe_email,
+ email: Faker::Internet.email,
confirmed_at: user.confirmed_at,
user_id: -1,
)
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index c704ffd21d8..c4f5f26f714 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -11,21 +11,27 @@
let(:threatmetrix) { true }
let(:idv_level) { 'in_person' }
let(:threatmetrix_response) do
- { client: nil,
+ {
+ client: nil,
errors: {},
exception: nil,
response_body: { "fraudpoint.score": '500',
request_id: '1234',
request_result: 'success',
+ account_lex_id: 'super-cool-test-lex-id',
+ session_id: 'super-cool-test-session-id',
review_status: 'pass',
risk_rating: 'trusted',
summary_risk_score: '-6',
tmx_risk_rating: 'neutral',
tmx_summary_reason_code: ['Identity_Negative_History'] },
review_status: 'pass',
+ account_lex_id: 'super-cool-test-lex-id',
+ session_id: 'super-cool-test-session-id',
success: true,
timed_out: false,
- transaction_id: 'ddp-mock-transaction-id-123' }
+ transaction_id: 'ddp-mock-transaction-id-123',
+ }
end
let(:base_proofing_components) do
{
@@ -767,14 +773,18 @@
let(:proofing_device_profiling) { :disabled }
let(:threatmetrix) { false }
let(:threatmetrix_response) do
- { client: 'tmx_disabled',
+ {
+ client: 'tmx_disabled',
success: true,
errors: {},
exception: nil,
timed_out: false,
transaction_id: nil,
review_status: 'pass',
- response_body: nil }
+ account_lex_id: nil,
+ session_id: nil,
+ response_body: nil,
+ }
end
it 'records all of the events' do
@@ -843,14 +853,18 @@
let(:proofing_device_profiling) { :disabled }
let(:threatmetrix) { false }
let(:threatmetrix_response) do
- { client: 'tmx_disabled',
+ {
+ client: 'tmx_disabled',
success: true,
errors: {},
exception: nil,
timed_out: false,
transaction_id: nil,
review_status: 'pass',
- response_body: nil }
+ account_lex_id: nil,
+ session_id: nil,
+ response_body: nil,
+ }
end
it 'records all of the events' do
@@ -888,14 +902,18 @@
let(:proofing_device_profiling) { :disabled }
let(:threatmetrix) { false }
let(:threatmetrix_response) do
- { client: 'tmx_disabled',
+ {
+ client: 'tmx_disabled',
success: true,
errors: {},
exception: nil,
timed_out: false,
transaction_id: nil,
review_status: 'pass',
- response_body: nil }
+ account_lex_id: nil,
+ session_id: nil,
+ response_body: nil,
+ }
end
it 'records all of the events' do
@@ -945,14 +963,18 @@
let(:idv_level) { 'legacy_in_person' }
let(:threatmetrix) { false }
let(:threatmetrix_response) do
- { client: 'tmx_disabled',
+ {
+ client: 'tmx_disabled',
success: true,
errors: {},
exception: nil,
timed_out: false,
transaction_id: nil,
review_status: 'pass',
- response_body: nil }
+ account_lex_id: nil,
+ session_id: nil,
+ response_body: nil,
+ }
end
it 'records all of the events', allow_browser_log: true do
@@ -1012,14 +1034,18 @@ def wait_for_event(event, wait)
let(:proofing_device_profiling) { :disabled }
let(:threatmetrix) { false }
let(:threatmetrix_response) do
- { client: 'tmx_disabled',
+ {
+ client: 'tmx_disabled',
success: true,
errors: {},
exception: nil,
timed_out: false,
transaction_id: nil,
review_status: 'pass',
- response_body: nil }
+ account_lex_id: nil,
+ session_id: nil,
+ response_body: nil,
+ }
end
it 'records all of the events' do
@@ -1069,14 +1095,18 @@ def wait_for_event(event, wait)
let(:proofing_device_profiling) { :disabled }
let(:threatmetrix) { false }
let(:threatmetrix_response) do
- { client: 'tmx_disabled',
+ {
+ client: 'tmx_disabled',
success: true,
errors: {},
exception: nil,
timed_out: false,
transaction_id: nil,
review_status: 'pass',
- response_body: nil }
+ account_lex_id: nil,
+ session_id: nil,
+ response_body: nil,
+ }
end
it 'records all of the events' do
diff --git a/spec/features/idv/steps/in_person/state_id_50_50_spec.rb b/spec/features/idv/steps/in_person/state_id_50_50_spec.rb
index d08dd7c2a71..61862486724 100644
--- a/spec/features/idv/steps/in_person/state_id_50_50_spec.rb
+++ b/spec/features/idv/steps/in_person/state_id_50_50_spec.rb
@@ -71,11 +71,8 @@
page.refresh
end
- it 'renders the 404 page' do
- expect(page).to have_content(
- "The page you were looking for doesn’t exist.\nYou might want to double-check your link" \
- " and try again. (404)",
- )
+ it 'renders the state ID controller page without error' do
+ expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
end
end
diff --git a/spec/features/remember_device/revocation_spec.rb b/spec/features/remember_device/revocation_spec.rb
index 1c83a366484..51a4073f2a0 100644
--- a/spec/features/remember_device/revocation_spec.rb
+++ b/spec/features/remember_device/revocation_spec.rb
@@ -62,7 +62,7 @@ def expect_mfa_to_be_required_for_user(user)
elsif TwoFactorAuthentication::WebauthnPolicy.new(user).platform_enabled?
login_two_factor_webauthn_path(platform: true)
elsif TwoFactorAuthentication::WebauthnPolicy.new(user).enabled?
- login_two_factor_webauthn_path(platform: false)
+ login_two_factor_webauthn_path
elsif TwoFactorAuthentication::AuthAppPolicy.new(user).enabled?
login_two_factor_authenticator_path
elsif TwoFactorAuthentication::PhonePolicy.new(user).enabled?
diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb
index b71f4ce0a78..b6fef7c4ab1 100644
--- a/spec/features/users/sign_up_spec.rb
+++ b/spec/features/users/sign_up_spec.rb
@@ -37,7 +37,7 @@
end
context 'picking a preferred email language on signup' do
- let(:email) { Faker::Internet.safe_email }
+ let(:email) { Faker::Internet.email }
it 'allows a user to pick a language when entering email' do
visit sign_up_email_path
diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb
index 1ad7393b430..57b52cbb92b 100644
--- a/spec/features/webauthn/hidden_spec.rb
+++ b/spec/features/webauthn/hidden_spec.rb
@@ -93,15 +93,40 @@
end
context 'with device that doesnt support authenticator' do
- it 'redirects to options page on sign in and shows the option' do
- email ||= user.email_addresses.first.email
- password = user.password
+ it 'redirects to options page and allows them to choose authenticator' do
visit new_user_session_path
set_hidden_field('platform_authenticator_available', 'false')
- fill_in_credentials_and_submit(email, password)
- continue_as(email, password)
+ fill_in_credentials_and_submit(user.email, user.password)
+
+ # Redirected to options page
expect(current_path).to eq(login_two_factor_options_path)
+
+ # Can choose authenticator
expect(webauthn_option_hidden?).to eq(false)
+ choose t('two_factor_authentication.login_options.webauthn_platform')
+ click_continue
+ expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ end
+
+ context 'if the webauthn credential is not their default mfa method when signing in' do
+ let(:user) do
+ create(:user, :fully_registered, :with_piv_or_cac, :with_webauthn_platform)
+ end
+
+ it 'allows them to choose authenticator if they change from their default method' do
+ visit new_user_session_path
+ set_hidden_field('platform_authenticator_available', 'false')
+ fill_in_credentials_and_submit(user.email, user.password)
+
+ # Redirected to default MFA method
+ expect(current_path).to eq(login_two_factor_piv_cac_path)
+
+ # Can change to authenticator if they choose
+ click_on t('two_factor_authentication.login_options_link_text')
+ choose t('two_factor_authentication.login_options.webauthn_platform')
+ click_continue
+ expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ end
end
end
end
diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/successful_redacted_response.json b/spec/fixtures/proofing/lexis_nexis/ddp/successful_redacted_response.json
index a58c4d1569b..5ee88beed85 100644
--- a/spec/fixtures/proofing/lexis_nexis/ddp/successful_redacted_response.json
+++ b/spec/fixtures/proofing/lexis_nexis/ddp/successful_redacted_response.json
@@ -6,5 +6,7 @@
"summary_risk_score": "-6",
"tmx_risk_rating": "neutral",
"fraudpoint.score": "500",
- "tmx_summary_reason_code": ["Identity_Negative_History"]
+ "tmx_summary_reason_code": ["Identity_Negative_History"],
+ "session_id": "super-cool-test-session-id",
+ "account_lex_id": "super-cool-test-lex-id"
}
diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/successful_response.json b/spec/fixtures/proofing/lexis_nexis/ddp/successful_response.json
index a408987796b..2de77c0ff61 100644
--- a/spec/fixtures/proofing/lexis_nexis/ddp/successful_response.json
+++ b/spec/fixtures/proofing/lexis_nexis/ddp/successful_response.json
@@ -7,5 +7,7 @@
"tmx_risk_rating": "neutral",
"fraudpoint.score": "500",
"first_name": "WARNING! YOU SHOULD NEVER SEE THIS PII FIELD IN THE LOGS",
- "tmx_summary_reason_code": ["Identity_Negative_History"]
+ "tmx_summary_reason_code": ["Identity_Negative_History"],
+ "session_id": "super-cool-test-session-id",
+ "account_lex_id": "super-cool-test-lex-id"
}
diff --git a/spec/jobs/reports/mfa_report_spec.rb b/spec/jobs/reports/mfa_report_spec.rb
new file mode 100644
index 00000000000..7c688ce2e5f
--- /dev/null
+++ b/spec/jobs/reports/mfa_report_spec.rb
@@ -0,0 +1,63 @@
+require 'rails_helper'
+
+RSpec.describe Reports::MfaReport do
+ let(:issuer) { 'issuer1' }
+ let(:issuers) { [issuer] }
+ let(:report_date) { Date.new(2023, 12, 0o1).in_time_zone('UTC') }
+ let(:email) { 'partner.name@example.com' }
+ let(:name) { 'Partner Name' }
+
+ let(:report_configs) do
+ [
+ {
+ 'name' => name,
+ 'issuers' => issuers,
+ 'emails' => [email],
+ },
+ ]
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:s3_reports_enabled).and_return(true)
+ allow(IdentityConfig.store).to receive(:mfa_report_config) { report_configs }
+ end
+
+ describe '#perform' do
+ let(:reports) { double('emailable_reports').as_null_object }
+
+ let(:monthly_report) { double(Reporting::MfaReport, as_emailable_reports: reports) }
+
+ before do
+ expect(Reporting::MfaReport).to receive(:new).with(
+ issuers:,
+ time_range: report_date.all_month,
+ ) { monthly_report }
+
+ allow(ReportMailer).to receive(:tables_report).and_call_original
+ end
+
+ it 'emails the csv' do
+ expect(ReportMailer).to receive(:tables_report).with(
+ email:,
+ subject: "Monthly MFA Report - #{report_date}",
+ message: "Report: mfa-report #{report_date}",
+ reports:,
+ attachment_format: :csv,
+ )
+
+ subject.perform(report_date)
+ end
+ end
+
+ describe 'with empty logs' do
+ before do
+ stub_cloudwatch_logs([])
+ end
+
+ it 'sends an email with at least 1 attachment' do
+ subject.perform(report_date)
+ sent_mail = ActionMailer::Base.deliveries.last
+ expect(sent_mail.parts.attachments.count).to be >= 1
+ end
+ end
+end
diff --git a/spec/presenters/two_factor_options_presenter_spec.rb b/spec/presenters/two_factor_options_presenter_spec.rb
index 5816a8da51d..a86ec05b8d3 100644
--- a/spec/presenters/two_factor_options_presenter_spec.rb
+++ b/spec/presenters/two_factor_options_presenter_spec.rb
@@ -116,23 +116,6 @@
]
end
end
- context 'when platform_auth_set_up_enabled is enabled' do
- before do
- allow(IdentityConfig.store).to receive(:platform_auth_set_up_enabled).
- and_return(true)
- end
-
- it 'supplies all the options except webauthn' do
- expect(presenter.options.map(&:class)).to eq [
- TwoFactorAuthentication::SetUpWebauthnPlatformSelectionPresenter,
- TwoFactorAuthentication::SetUpAuthAppSelectionPresenter,
- TwoFactorAuthentication::SetUpPhoneSelectionPresenter,
- TwoFactorAuthentication::SetUpWebauthnSelectionPresenter,
- TwoFactorAuthentication::SetUpPivCacSelectionPresenter,
- TwoFactorAuthentication::SetUpBackupCodeSelectionPresenter,
- ]
- end
- end
end
describe '#all_options_sorted' do
diff --git a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
index df0ca8d7955..c57e331ba28 100644
--- a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
+++ b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
@@ -99,6 +99,9 @@
expect(result.success?).to eq(true)
expect(result.errors).to be_empty
+ expect(result.review_status).to eq('pass')
+ expect(result.session_id).to eq('super-cool-test-session-id')
+ expect(result.account_lex_id).to eq('super-cool-test-lex-id')
end
end
diff --git a/spec/services/proofing/socure/id_plus/input_spec.rb b/spec/services/proofing/socure/id_plus/input_spec.rb
index ba709a98f5b..2aaeadefa49 100644
--- a/spec/services/proofing/socure/id_plus/input_spec.rb
+++ b/spec/services/proofing/socure/id_plus/input_spec.rb
@@ -4,7 +4,7 @@
let(:user) { build(:user) }
let(:state_id) do
- Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(consent_given_at: '2024-09-01T00:00:00Z')
end
subject do
@@ -32,6 +32,8 @@
phone: '12025551212',
ssn: '900-66-1234',
email: user.email,
+
+ consent_given_at: '2024-09-01T00:00:00Z',
},
)
end
diff --git a/spec/services/proofing/socure/id_plus/request_spec.rb b/spec/services/proofing/socure/id_plus/request_spec.rb
index 4c0fd4ec867..7ff73f5d5e8 100644
--- a/spec/services/proofing/socure/id_plus/request_spec.rb
+++ b/spec/services/proofing/socure/id_plus/request_spec.rb
@@ -15,7 +15,9 @@
let(:input) do
Proofing::Socure::IdPlus::Input.new(
email: user.email,
- **Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.slice(
+ **Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(
+ consent_given_at: '2024-09-01T00:00:00Z',
+ ).slice(
*Proofing::Socure::IdPlus::Input.members,
),
)
@@ -27,37 +29,30 @@
describe '#body' do
it 'contains all expected values' do
- freeze_time do
- expect(JSON.parse(request.body, symbolize_names: true)).to eql(
- {
- modules: [
- 'kyc',
- ],
- firstName: 'FAKEY',
- surName: 'MCFAKERSON',
- dob: '1938-10-06',
- physicalAddress: '1 FAKE RD',
- physicalAddress2: nil,
- city: 'GREAT FALLS',
- state: 'MT',
- zip: '59010-1234',
- country: 'US',
- nationalId: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:ssn],
- countryOfOrigin: 'US',
-
- email: user.email,
- mobileNumber: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:phone],
-
- userConsent: true,
-
- # XXX: This should be set to the time the user submitted agreement,
- # which we are not currently tracking. The "5.minutes.ago" is
- # because Socure will reject times "in the future", so we avoid
- # our clocks being out of sync with theirs.
- consentTimestamp: 5.minutes.ago.iso8601,
- },
- )
- end
+ expect(JSON.parse(request.body, symbolize_names: true)).to eql(
+ {
+ modules: [
+ 'kyc',
+ ],
+ firstName: 'FAKEY',
+ surName: 'MCFAKERSON',
+ dob: '1938-10-06',
+ physicalAddress: '1 FAKE RD',
+ physicalAddress2: nil,
+ city: 'GREAT FALLS',
+ state: 'MT',
+ zip: '59010-1234',
+ country: 'US',
+ nationalId: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:ssn],
+ countryOfOrigin: 'US',
+
+ email: user.email,
+ mobileNumber: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:phone],
+
+ userConsent: true,
+ consentTimestamp: '2024-09-01T00:00:00+00:00',
+ },
+ )
end
end
diff --git a/spec/services/push_notification/email_changed_event_spec.rb b/spec/services/push_notification/email_changed_event_spec.rb
index 20bfd52fdab..c673a3e4015 100644
--- a/spec/services/push_notification/email_changed_event_spec.rb
+++ b/spec/services/push_notification/email_changed_event_spec.rb
@@ -9,7 +9,7 @@
end
let(:user) { build(:user) }
- let(:email) { Faker::Internet.safe_email }
+ let(:email) { Faker::Internet.email }
describe '#event_type' do
it 'is the RISC event type' do
diff --git a/spec/services/push_notification/http_push_spec.rb b/spec/services/push_notification/http_push_spec.rb
index 6a854c044b4..1cd1e71bb13 100644
--- a/spec/services/push_notification/http_push_spec.rb
+++ b/spec/services/push_notification/http_push_spec.rb
@@ -18,7 +18,7 @@
let(:event) do
PushNotification::IdentifierRecycledEvent.new(
user: user,
- email: Faker::Internet.safe_email,
+ email: Faker::Internet.email,
)
end
let(:now) { Time.zone.now }
diff --git a/spec/services/push_notification/identifier_recycled_event_spec.rb b/spec/services/push_notification/identifier_recycled_event_spec.rb
index 548fbc57eea..a99664d4492 100644
--- a/spec/services/push_notification/identifier_recycled_event_spec.rb
+++ b/spec/services/push_notification/identifier_recycled_event_spec.rb
@@ -9,7 +9,7 @@
end
let(:user) { build(:user) }
- let(:email) { Faker::Internet.safe_email }
+ let(:email) { Faker::Internet.email }
describe '#event_type' do
it 'is the RISC event type' do
diff --git a/spec/services/reset_user_password_spec.rb b/spec/services/reset_user_password_spec.rb
index f7aafe5f929..f0017af9340 100644
--- a/spec/services/reset_user_password_spec.rb
+++ b/spec/services/reset_user_password_spec.rb
@@ -20,7 +20,7 @@
end
it 'notifies the user via email to each of their confirmed email addresses' do
- create(:email_address, user:, email: Faker::Internet.safe_email, confirmed_at: nil)
+ create(:email_address, user:, email: Faker::Internet.email, confirmed_at: nil)
expect { call }.
to(change { ActionMailer::Base.deliveries.count }.by(2))
diff --git a/spec/services/usps_in_person_proofing/proofer_spec.rb b/spec/services/usps_in_person_proofing/proofer_spec.rb
index e8fe2c16a9a..a31e7fe7d88 100644
--- a/spec/services/usps_in_person_proofing/proofer_spec.rb
+++ b/spec/services/usps_in_person_proofing/proofer_spec.rb
@@ -95,7 +95,7 @@ def expect_facility_fields_to_be_present(facility)
zip_code: Faker::Address.zip_code,
first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
- email: Faker::Internet.safe_email,
+ email: Faker::Internet.email,
unique_id: '123456789',
)
stub_request_token
@@ -291,7 +291,7 @@ def expect_facility_fields_to_be_present(facility)
zip_code: Faker::Address.zip_code,
first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
- email: Faker::Internet.safe_email,
+ email: Faker::Internet.email,
unique_id: '123456789',
)
end
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index d2ffb80101a..ea92023dd69 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -122,7 +122,7 @@ def fill_in_password_and_submit(password)
end
def sign_up
- email = Faker::Internet.safe_email
+ email = Faker::Internet.email
sign_up_with(email)
confirm_last_user
end
diff --git a/spec/support/flow_policy_helper.rb b/spec/support/flow_policy_helper.rb
index 67a24a8a6d6..ecda95a489e 100644
--- a/spec/support/flow_policy_helper.rb
+++ b/spec/support/flow_policy_helper.rb
@@ -12,7 +12,7 @@ def stub_step(key:, idv_session:)
when :welcome
idv_session.welcome_visited = true
when :agreement
- idv_session.idv_consent_given_at = Time.zone.now
+ idv_session.idv_consent_given_at = Time.zone.now.to_s
when :how_to_verify
idv_session.skip_doc_auth = false
idv_session.skip_doc_auth_from_how_to_verify = false
diff --git a/spec/support/shared_examples/remember_device.rb b/spec/support/shared_examples/remember_device.rb
index 79719d93e92..8cadc69f64f 100644
--- a/spec/support/shared_examples/remember_device.rb
+++ b/spec/support/shared_examples/remember_device.rb
@@ -70,7 +70,7 @@ def expect_mfa_to_be_required_for_user(user)
elsif TwoFactorAuthentication::WebauthnPolicy.new(user).platform_enabled?
login_two_factor_webauthn_path(platform: true)
elsif TwoFactorAuthentication::WebauthnPolicy.new(user).enabled?
- login_two_factor_webauthn_path(platform: false)
+ login_two_factor_webauthn_path
elsif TwoFactorAuthentication::AuthAppPolicy.new(user).enabled?
login_two_factor_authenticator_path
elsif TwoFactorAuthentication::PhonePolicy.new(user).enabled?