Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d0ee6b2
personal-key repo
peggles2 Apr 11, 2022
cffdff9
update route to verify/complete/personal_key
peggles2 Apr 12, 2022
7d0e053
get request working with postman to return empty hash for now
peggles2 Apr 12, 2022
d48300f
add_proofing_component to get_personal_key
peggles2 Apr 12, 2022
0473837
add analytics for personal key
peggles2 Apr 12, 2022
02b6a62
update complete_controller
peggles2 Apr 15, 2022
eb35ad4
update error messages for the 2 factor auth so api can get proper jso…
peggles2 Apr 15, 2022
1d1e3fa
cleanup code
peggles2 Apr 15, 2022
1e52308
update the application controller
peggles2 Apr 15, 2022
a8f1aee
changes made to return proper json error responses
peggles2 Apr 15, 2022
6cc1dda
latest changes
peggles2 Apr 18, 2022
3bbd31e
update code
peggles2 Apr 18, 2022
7f1d382
create the profile and cache the pii
solipet Apr 18, 2022
33eab3e
create the profile creation form correctly
solipet Apr 18, 2022
89f4640
clean up the JWT code, use idv certificate pair
solipet Apr 18, 2022
750a933
update code
peggles2 Apr 18, 2022
30b83df
Merge branch 'main' of github.com:18F/identity-idp into lg-6114-perso…
peggles2 Apr 19, 2022
0fe9764
changes made to cleanup code
peggles2 Apr 19, 2022
c06c5c6
Merge branch 'main' of github.com:18F/identity-idp into lg-6114-perso…
peggles2 Apr 19, 2022
0b87d4f
update FormResponse to return a {} if extra_attributes is nil
peggles2 Apr 19, 2022
c201eb9
changes made to fix the correct jwt to return user key
peggles2 Apr 19, 2022
4928fdc
specs for Api::ProfileCreationForm
solipet Apr 19, 2022
ba29220
specs for Api::ProfileCreationForm (for reals)
solipet Apr 20, 2022
91ea768
Merge branch 'main' of github.com:18F/identity-idp into lg-6114-perso…
peggles2 Apr 20, 2022
5c44e19
Merge branch 'lg-6114-personal-key' of github.com:18F/identity-idp in…
peggles2 Apr 20, 2022
f24ca6d
add rspec tests
peggles2 Apr 20, 2022
2b7abf7
cleanup test
peggles2 Apr 20, 2022
432732b
cleanup code
peggles2 Apr 21, 2022
d3882dd
fix profile creation form spec on recovery key
solipet Apr 21, 2022
57c8ff7
changes made to make it a post instead of a get
peggles2 Apr 21, 2022
2aead0d
cleanup lint
peggles2 Apr 21, 2022
faf002c
fix some more linter errors
peggles2 Apr 21, 2022
3f00de9
Include "personal_key" as alertable key in analytics PiiDetector
aduth Apr 22, 2022
078af46
Revert "Include "personal_key" as alertable key in analytics PiiDetec…
aduth Apr 22, 2022
35b064c
implement/test complete_session
solipet Apr 22, 2022
e103006
changes made to fix the code review feedacks
peggles2 Apr 22, 2022
4c22483
fix conflicts
peggles2 Apr 22, 2022
b2ecfe1
get rid of aliased methods
solipet Apr 22, 2022
000690d
lints
solipet Apr 22, 2022
d12fee1
fix linter error
peggles2 Apr 22, 2022
37a9df6
Merge branch 'lg-6114-personal-key' of github.com:18F/identity-idp in…
peggles2 Apr 22, 2022
f0c8b47
remove parenthesis
peggles2 Apr 22, 2022
31c71bc
code review feedback
peggles2 Apr 22, 2022
310e6df
changelog: Upcoming Features, Identity Verification, API endpoint for…
peggles2 Apr 22, 2022
a4be0be
fix line space
peggles2 Apr 22, 2022
4ee9fb0
code review feedback
peggles2 Apr 22, 2022
784861f
fix lint error
peggles2 Apr 23, 2022
6a0e7b8
add feature flagging
peggles2 Apr 25, 2022
609480a
move the personal_key to a dedicated method, encapsulate the JWT in a…
solipet Apr 26, 2022
b4fc505
lints
solipet Apr 26, 2022
a43e859
convert profile_completion_form to return the personal_key separately…
solipet Apr 26, 2022
b8eb235
remove unused custom form response class
solipet Apr 27, 2022
0784d74
Update config/routes.rb
solipet Apr 27, 2022
a21836c
Update app/forms/api/profile_creation_form.rb
solipet Apr 27, 2022
ddaad6b
remove `gpo_otp` as a method on the form
solipet Apr 27, 2022
65018ce
move the feature flag check from routes.rb to the controller
solipet Apr 27, 2022
c1fecba
Merge branch 'main' into lg-6114-personal-key
solipet Apr 27, 2022
d5ae4c7
remove unnecessary session usage
solipet Apr 28, 2022
5683aad
default keys for IdV JWTs
solipet Apr 28, 2022
67a3940
guard against small IdV JWT keys in production envs
solipet Apr 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Api
class BaseController < ApplicationController
before_action :check_api_enabled
before_action :confirm_two_factor_authenticated_for_api

respond_to :json

def check_api_enabled
render_api_not_found unless IdentityConfig.store.idv_api_enabled
end

def confirm_two_factor_authenticated_for_api
return if user_fully_authenticated?
render json: { error: 'user is not fully authenticated' }, status: :unauthorized
end

def render_api_not_found
render json: { error: "The page you were looking for doesn't exist" }, status: :not_found
end
end
end
34 changes: 34 additions & 0 deletions app/controllers/api/verify/complete_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Api
module Verify
class CompleteController < Api::BaseController
def create
result, personal_key = Api::ProfileCreationForm.new(
password: verify_params[:password],
jwt: verify_params[:details],
user_session: user_session,
service_provider: current_sp,
).submit

if result.success?
user = User.find_by(uuid: result.extra[:user_uuid])
add_proofing_component(user)
render json: { personal_key: personal_key,
profile_pending: result.extra[:profile_pending] },
status: :ok
else
render json: { error: result.errors }, status: :bad_request
end
end

private

def verify_params
params.permit(:password, :details)
end

def add_proofing_component(user)
ProofingComponent.create_or_find_by(user: user).update(verified_at: Time.zone.now)
end
end
end
end
49 changes: 49 additions & 0 deletions app/decorators/api/user_bundle_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module Api
class UserBundleError < StandardError; end

class UserBundleDecorator
# Note, does not rescue JWT errors - responsibility of the user
def initialize(user_bundle:, public_key:)
payload, headers = JWT.decode(
user_bundle,
public_key,
true,
algorithm: 'RS256',
)
@jwt_payload = payload
@jwt_headers = headers

raise UserBundleError.new('pii is missing') unless jwt_payload['pii']
raise UserBundleError.new('metadata is missing') unless jwt_payload['metadata']
end

def gpo_address_verification?
metadata[:address_verification_mechanism] == 'gpo'
end

def pii
HashWithIndifferentAccess.new(jwt_payload['pii'])
end

def user
return @user if defined?(@user)
@user = User.find_by(uuid: jwt_headers['sub'])
end

def user_phone_confirmation?
metadata[:user_phone_confirmation] == true
end

def vendor_phone_confirmation?
metadata[:vendor_phone_confirmation] == true
end

private

attr_reader :jwt_payload, :jwt_headers

def metadata
HashWithIndifferentAccess.new(jwt_payload['metadata'])
end
end
end
155 changes: 155 additions & 0 deletions app/forms/api/profile_creation_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
module Api
class ProfileCreationForm
include ActiveModel::Model

validate :valid_jwt
validate :valid_user
validate :valid_password

attr_reader :password, :user_bundle
attr_reader :user_session, :service_provider
attr_reader :profile

def initialize(password:, jwt:, user_session:, service_provider: nil)
@password = password
@jwt = jwt
@user_session = user_session
@service_provider = service_provider
set_idv_session
end

def submit
@form_valid = valid?

if form_valid?
create_profile
cache_encrypted_pii
complete_session
end

response = FormResponse.new(
success: form_valid?,
errors: errors.to_hash,
extra: extra_attributes,
)
[response, personal_key]
end

private

attr_reader :jwt

def create_profile
profile_maker = build_profile_maker
profile = profile_maker.save_profile
@profile = profile
session[:pii] = profile_maker.pii_attributes
session[:profile_id] = profile.id
session[:personal_key] = profile.personal_key
end

def cache_encrypted_pii
cacher = Pii::Cacher.new(user, session)
cacher.save(password, profile)
end

def complete_session
complete_profile if phone_confirmed?
create_gpo_entry if user_bundle.gpo_address_verification?
end

def phone_confirmed?
user_bundle.vendor_phone_confirmation? && user_bundle.user_phone_confirmation?
end

def complete_profile
user.pending_profile&.activate
move_pii_to_user_session
end

def move_pii_to_user_session
return if session[:decrypted_pii].blank?
user_session[:decrypted_pii] = session.delete(:decrypted_pii)
end

def create_gpo_entry
move_pii_to_user_session
confirmation_maker = GpoConfirmationMaker.new(
pii: Pii::Cacher.new(user, user_session).fetch,
service_provider: service_provider,
profile: profile,
)
confirmation_maker.perform
end

def build_profile_maker
Idv::ProfileMaker.new(
applicant: user_bundle.pii,
user: user,
user_password: password,
)
end

def user
user_bundle&.user
end

def set_idv_session
return if session.present?
user_session[:idv] = {}
end

def session
user_session.fetch(:idv, {})
end

def valid_jwt
@user_bundle = Api::UserBundleDecorator.new(user_bundle: jwt, public_key: public_key)
rescue JWT::DecodeError => err
errors.add(:jwt, "decode error: #{err.message}", type: :invalid)
rescue ::Api::UserBundleError => err
errors.add(:jwt, "malformed user bundle: #{err.message}", type: :invalid)
end

def valid_user
return if user
errors.add(:user, 'user not found', type: :invalid)
end

def valid_password
return if user&.valid_password?(password)
errors.add(:password, 'invalid password', type: :invalid)
end

def form_valid?
@form_valid
end

def extra_attributes
if user.present?
@extra_attributes ||= {
profile_pending: user.pending_profile?,
user_uuid: user.uuid,
}
else
@extra_attributes = {}
end
end

def personal_key
@personal_key ||= profile&.personal_key || profile&.encrypt_recovery_pii(pii)
end

def public_key
key = OpenSSL::PKey::RSA.new(Base64.strict_decode64(IdentityConfig.store.idv_public_key))

if Identity::Hostdata.in_datacenter?
env = Identity::Hostdata.env
prod_env = env == 'prod' || env == 'staging' || env == 'dm'
raise 'key size too small' if prod_env && key.n.num_bits < 2048
end

key
end
end
end
2 changes: 2 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ idv_api_enabled: false
idv_attempt_window_in_hours: 6
idv_max_attempts: 5
idv_min_age_years: 13
idv_public_key: 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBS3p4d25rbUxqeGx1NmhsRlQ2d2JreUlweHNtYkMyaApjYW5TMGhuWm1DRGIrTEhaME5zQTdHWURpZkMxQlRBMHRuRFo0Zm9HNTRmYjNzYk9ubGpGWXVNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='
idv_private_key: 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT2dJQkFBSkJBS3p4d25rbUxqeGx1NmhsRlQ2d2JreUlweHNtYkMyaGNhblMwaG5abUNEYitMSFowTnNBCjdHWURpZkMxQlRBMHRuRFo0Zm9HNTRmYjNzYk9ubGpGWXVNQ0F3RUFBUUpCQUp6TUMvOSs2RWlHQzkrZTFlWWkKVzc0ejN4MjBkanZndFlhOHh4UDh2ZnA3TjdKQXMvaGNUbjVLOCtDM2swaXUyR2RNb21qSlp2ckxwT0IyTWh4RQo3QkVDSVFEVERhbVRCMHhKSlVpV0ljNk15Y0dFa2J4SEZ3eEtURVNCaHhzREFISDZEUUloQU5IR2NwVUs5dmVSCkdrZlZTOS9MSVNZQlk2YzRUZk1NUFJZU21KVHFNRVN2QWlBZFdiY05aV1JzZjZ6YWhCVVBhemRvVWtRV3R0UFUKdVVxRm9ONVd5b2NQT1FJZ1FrUjlaK1haMUtVcTl5eERWc1FWaWFzQXJ3K1RXRWN5ZU9tUTkrSHZNNU1DSUcrMQpxVldqNW9PL0FBSU1QbXZVZmp5L0JnMnhEQVRiOEp6alFrQ3dLSnNwCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=='
idv_send_link_attempt_window_in_minutes: 10
idv_send_link_max_attempts: 5
in_person_proofing_enabled: true
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@
].each { |step_path| get step_path => 'verify#show' }
end

namespace :api do
post '/verify/complete' => 'verify/complete#create'
end

get '/account/verify' => 'idv/gpo_verify#index', as: :idv_gpo_verify
post '/account/verify' => 'idv/gpo_verify#create'
if FeatureManagement.enable_gpo_verification?
Expand Down
2 changes: 2 additions & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ def self.build_store(config_map)
config.add(:idv_attempt_window_in_hours, type: :integer)
config.add(:idv_max_attempts, type: :integer)
config.add(:idv_min_age_years, type: :integer)
config.add(:idv_private_key, type: :string)
config.add(:idv_public_key, type: :string)
config.add(:idv_send_link_attempt_window_in_minutes, type: :integer)
config.add(:idv_send_link_max_attempts, type: :integer)
config.add(:in_person_proofing_enabled, type: :boolean)
Expand Down
87 changes: 87 additions & 0 deletions spec/controllers/api/verify/complete_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'rails_helper'

describe Api::Verify::CompleteController do
include PersonalKeyValidator
include SamlAuthHelper

def stub_idv_session
stub_sign_in(user)
end

let(:password) { 'iambatman' }
let(:user) { create(:user, :signed_up, password: password) }
let(:applicant) do
{ first_name: 'Bruce',
last_name: 'Wayne',
address1: '123 Mansion St',
address2: 'Ste 456',
city: 'Gotham City',
state: 'NY',
zipcode: '10015' }
end

let(:pii) do
{ first_name: 'Bruce',
last_name: 'Wayne',
ssn: '900-90-1234' }
end

let(:profile) { subject.idv_session.profile }
let(:key) { OpenSSL::PKey::RSA.new(Base64.strict_decode64(IdentityConfig.store.idv_private_key)) }
let(:jwt) { JWT.encode({ pii: pii, metadata: {} }, key, 'RS256', sub: user.uuid) }

before do
allow(IdentityConfig.store).to receive(:idv_api_enabled).and_return(true)
end

describe 'before_actions' do
it 'includes before_actions from Api::BaseController' do
expect(subject).to have_actions(
:before,
:confirm_two_factor_authenticated_for_api,
)
end
end

describe '#create' do
context 'when the user is not signed in and submits the password' do
it 'does not create a profile or return a key' do
post :create, params: { password: 'iambatman', details: jwt }
expect(JSON.parse(response.body)['personal_key']).to be_nil
expect(response.status).to eq 401
expect(JSON.parse(response.body)['error']).to eq 'user is not fully authenticated'
end
end

context 'when the user is signed in and submits the password' do
before do
stub_idv_session
end

it 'creates a profile and returns a key' do
post :create, params: { password: 'iambatman', details: jwt }
expect(JSON.parse(response.body)['personal_key']).not_to be_nil
expect(response.status).to eq 200
end

it 'does not create a profile and return a key when it has the wrong password' do
post :create, params: { password: 'iamnotbatman', details: jwt }
expect(JSON.parse(response.body)['personal_key']).to be_nil
expect(response.status).to eq 400
end
end

context 'when the idv api is not enabled' do
before do
allow(IdentityConfig.store).to receive(:idv_api_enabled).and_return(false)
end

it 'responds with not found' do
post :create, params: { password: 'iambatman', details: jwt }
expect(response.status).to eq 404
expect(JSON.parse(response.body)['error']).
to eq "The page you were looking for doesn't exist"
end
end
end
end
Loading