Skip to content
25 changes: 22 additions & 3 deletions app/forms/idv/ssn_form.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
module Idv
class SsnForm
include ActiveModel::Model
include FormSsnValidator

ATTRIBUTES = [:ssn].freeze

attr_accessor :ssn

validates :ssn, presence: true
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The profile step actually is not part of the doc auth flow (long story). We'll actually want the SSN unique bit in the extra attributes of the form response coming out of this form.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I got that in 9a1b1b4

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it, yeah

validates_format_of :ssn,
with: /\A\d{3}-?\d{2}-?\d{4}\z/,
message: I18n.t('idv.errors.pattern_mismatch.ssn'),
allow_blank: false

def self.model_name
ActiveModel::Name.new(self, nil, 'Ssn')
end
Expand All @@ -18,7 +22,22 @@ def initialize(user)
def submit(params)
consume_params(params)

FormResponse.new(success: valid?, errors: errors.messages)
FormResponse.new(success: valid?, errors: errors.messages, extra: extra_analytics_attributes)
end

def ssn_is_unique?
return false if ssn.nil?

@ssn_is_unique ||= DuplicateSsnFinder.new(
ssn: ssn,
user: @user,
).ssn_is_unique?
end

def extra_analytics_attributes
{
ssn_is_unique: ssn_is_unique?,
}
end

private
Expand Down
1 change: 0 additions & 1 deletion app/models/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class Profile < ApplicationRecord
has_many :usps_confirmation_codes, dependent: :destroy

validates :active, uniqueness: { scope: :user_id, if: :active? }
validates :ssn_signature, uniqueness: { scope: :active, if: :active? }

scope(:active, -> { where(active: true) })
scope(:verified, -> { where.not(verified_at: nil) })
Expand Down
12 changes: 1 addition & 11 deletions app/services/idv/profile_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,7 @@ def idv_throttle_params
end

def success?
idv_result[:success] && ssn_is_unique?
end

def ssn_is_unique?
ssn = applicant[:ssn]
return false if ssn.nil?

@ssn_is_unique ||= DuplicateSsnFinder.new(
ssn: ssn, user: idv_session.current_user,
).ssn_is_unique?
idv_result[:success]
end

def failed_due_to_timeout_or_exception?
Expand All @@ -70,7 +61,6 @@ def extra_analytics_attributes
{
idv_attempts_exceeded: throttled?,
vendor: idv_result.except(:errors, :success),
ssn_is_unique: ssn_is_unique?,
Comment thread
zachmargolis marked this conversation as resolved.
}
end
end
Expand Down
41 changes: 0 additions & 41 deletions app/validators/idv/form_ssn_validator.rb

This file was deleted.

1 change: 0 additions & 1 deletion config/locales/idv/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ en:
warning_5: You can manage or delete that account on your profile page
errors:
bad_dob: Your date of birth must be a valid date.
duplicate_ssn: An account already exists with the information you provided.
incorrect_password: The password you entered is not correct.
mail_limit_reached: You have have requested too much mail in the last month.
pattern_mismatch:
Expand Down
1 change: 0 additions & 1 deletion config/locales/idv/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ es:
warning_5: Puedes gestionar o eliminar esa cuenta desde tu página de perfil
errors:
bad_dob: Su fecha de nacimiento debe ser una fecha válida.
duplicate_ssn: Ya existe una cuenta con la información que proporcionó.
incorrect_password: La contraseña que ingresó no es correcta.
mail_limit_reached: Usted ha solicitado demasiado correo en el último mes.
pattern_mismatch:
Expand Down
2 changes: 0 additions & 2 deletions config/locales/idv/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ fr:
warning_5: Vous pouvez gérer ou supprimer ce compte sur votre page de profil
errors:
bad_dob: Votre date de naissance doit être une date valide.
duplicate_ssn: Un compte existe déjà avec l'information que vous avez fournie.
chiffres.
incorrect_password: Le mot de passe que vous avez inscrit est incorrect.
mail_limit_reached: Vous avez demandé trop de lettres au cours du dernier mois.
pattern_mismatch:
Expand Down
27 changes: 27 additions & 0 deletions db/migrate/20200321210321_drop_ssn_uniqueness_constraint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class DropSsnUniquenessConstraint < ActiveRecord::Migration[5.1]
disable_ddl_transaction!

def up
remove_index :profiles, %i[ssn_signature active]
remove_index :profiles, %i[user_id ssn_signature active]
end

def down
add_index(
:profiles,
%i[ssn_signature active],
name: :index_profiles_on_ssn_signature_and_active,
unique: true,
where: '(active = true)',
algorithm: :concurrently,
)
add_index(
:profiles,
%i[user_id ssn_signature active],
name: :index_profiles_on_user_id_and_ssn_signature_and_active,
unique: true,
where: '(active = true)',
algorithm: :concurrently,
)
end
end
4 changes: 1 addition & 3 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20200319233723) do
ActiveRecord::Schema.define(version: 2020_03_21_210321) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -321,10 +321,8 @@
t.integer "deactivation_reason"
t.boolean "phone_confirmed", default: false, null: false
t.jsonb "proofing_components"
t.index ["ssn_signature", "active"], name: "index_profiles_on_ssn_signature_and_active", unique: true, where: "(active = true)"
t.index ["ssn_signature"], name: "index_profiles_on_ssn_signature"
t.index ["user_id", "active"], name: "index_profiles_on_user_id_and_active", unique: true, where: "(active = true)"
t.index ["user_id", "ssn_signature", "active"], name: "index_profiles_on_user_id_and_ssn_signature_and_active", unique: true, where: "(active = true)"
t.index ["user_id"], name: "index_profiles_on_user_id"
end

Expand Down
26 changes: 0 additions & 26 deletions spec/controllers/idv/sessions_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,30 +85,6 @@
expect(subject.idv_session.applicant['uuid']).to eq subject.current_user.uuid
end

it 'redirects to failure if the SSN exists' do
create(:profile, pii: { ssn: '666-66-1234' })

context = { stages: [{ resolution: 'ResolutionMock' }, { state_id: 'StateIdMock' }] }
result = {
success: false,
idv_attempts_exceeded: false,
errors: {},
ssn_is_unique: false,
vendor: { messages: [], context: context, exception: nil, timed_out: false },
}

expect(@analytics).to receive(:track_event).ordered.
with(Analytics::IDV_BASIC_INFO_SUBMITTED_FORM, hash_including(success: true))
expect(@analytics).to receive(:track_event).ordered.
with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result)

post :create, params: { profile: user_attrs.merge(ssn: '666-66-1234') }

expect(response).to redirect_to(idv_session_errors_warning_url)
expect(idv_session.profile_confirmation).to be_falsy
expect(idv_session.resolution_successful).to be_falsy
end

it 'renders the forms if there are missing fields' do
partial_attrs = user_attrs.tap { |attrs| attrs.delete :first_name }

Expand Down Expand Up @@ -139,7 +115,6 @@
errors: {
first_name: ['Unverified first name.'],
},
ssn_is_unique: true,
vendor: { messages: [], context: context, exception: nil, timed_out: false },
}

Expand All @@ -161,7 +136,6 @@
success: true,
idv_attempts_exceeded: false,
errors: {},
ssn_is_unique: true,
vendor: { messages: [], context: context, exception: nil, timed_out: false },
}

Expand Down
28 changes: 0 additions & 28 deletions spec/features/idv/steps/profile_step_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,6 @@
end
end

context 'when an account exists with the same SSN' do
it 'renders a warning and locks the user out after 3 attempts' do
ssn = '123-45-6789'
create(:profile, ssn_signature: Pii::Fingerprinter.fingerprint(ssn))

start_idv_from_sp
complete_idv_steps_before_profile_step

2.times do
fill_out_idv_form_ok
fill_in :profile_ssn, with: ssn
click_continue

expect(page).to have_content(t('idv.failure.sessions.warning'))
expect(page).to have_current_path(idv_session_errors_warning_path)

click_on t('idv.failure.button.warning')
end

fill_out_idv_form_ok
fill_in :profile_ssn, with: ssn
click_continue

expect(page).to have_content(strip_tags(t('idv.failure.sessions.fail_html')))
expect(page).to have_current_path(idv_session_errors_failure_path)
end
end

context 'cancelling IdV' do
it_behaves_like 'cancel at idv step', :profile
it_behaves_like 'cancel at idv step', :profile, :oidc
Expand Down
11 changes: 0 additions & 11 deletions spec/features/users/user_profile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,6 @@
end
end

it 'prevents a user from using the same credentials to sign up' do
pii = { ssn: '1234', dob: '1920-01-01' }
profile = create(:profile, :active, :verified, pii: pii)
sign_in_live_with_2fa(profile.user)
click_link(t('links.sign_out'), match: :first)

expect do
create(:profile, :active, :verified, pii: pii)
end.to raise_error(ActiveRecord::RecordInvalid)
end

context 'ial2 user clicks the delete account button' do
it 'deletes the account and signs the user out with a flash message' do
profile = create(:profile, :active, :verified, pii: { ssn: '1234', dob: '1920-01-01' })
Expand Down
60 changes: 13 additions & 47 deletions spec/forms/idv/ssn_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
describe Idv::SsnForm do
let(:user) { create(:user) }
let(:subject) { Idv::SsnForm.new(user) }
let(:ssn) { '111-11-1111' }
let(:ssn) { '111111111' }

describe '#submit' do
context 'when the form is valid' do
it 'returns a successful form response' do
result = subject.submit(ssn: '111111111')
result = subject.submit(ssn: ssn)

expect(result).to be_kind_of(FormResponse)
expect(result.success?).to eq(true)
expect(result.errors).to be_empty
expect(result.extra).to eq(ssn_is_unique: true)
end

context 'when the SSN is a duplicate' do
before { create(:profile, pii: { ssn: ssn }) }

it 'logs that there is a duplicate SSN' do
result = subject.submit(ssn: ssn)
expect(result.extra).to eq(ssn_is_unique: false)
end
end
end

Expand All @@ -28,7 +38,7 @@

context 'when the form has invalid attributes' do
it 'raises an error' do
expect { subject.submit(ssn: '111111111', foo: 1) }.
expect { subject.submit(ssn: ssn, foo: 1) }.
to raise_error(ArgumentError, 'foo is an invalid ssn attribute')
end
end
Expand All @@ -41,48 +51,4 @@
expect(subject).to_not be_valid
end
end

describe 'ssn uniqueness' do
context 'when ssn is already taken by another profile' do
it 'is invalid' do
diff_user = create(:user)
create(:profile, pii: { ssn: ssn }, user: diff_user)

subject.submit(ssn: ssn)

expect(subject.valid?).to eq false
expect(subject.errors[:ssn]).to eq [t('idv.errors.duplicate_ssn')]
end

it 'recognizes fingerprint regardless of HMAC key age' do
diff_user = create(:user)
create(:profile, pii: { ssn: ssn }, user: diff_user)
rotate_hmac_key

subject.submit(ssn: ssn)

expect(subject.valid?).to eq false
expect(subject.errors[:ssn]).to eq [t('idv.errors.duplicate_ssn')]
end
end

context 'when ssn is already taken by same profile' do
it 'is valid' do
create(:profile, pii: { ssn: ssn }, user: user)

subject.submit(ssn: ssn)

expect(subject.valid?).to eq true
end

it 'recognizes fingerprint regardless of HMAC key age' do
create(:profile, pii: { ssn: ssn }, user: user)
rotate_hmac_key

subject.submit(ssn: ssn)

expect(subject.valid?).to eq true
end
end
end
end
Loading