-
Notifications
You must be signed in to change notification settings - Fork 166
Lg 6114 personal key #6237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Lg 6114 personal key #6237
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
d0ee6b2
personal-key repo
peggles2 cffdff9
update route to verify/complete/personal_key
peggles2 7d0e053
get request working with postman to return empty hash for now
peggles2 d48300f
add_proofing_component to get_personal_key
peggles2 0473837
add analytics for personal key
peggles2 02b6a62
update complete_controller
peggles2 eb35ad4
update error messages for the 2 factor auth so api can get proper jso…
peggles2 1d1e3fa
cleanup code
peggles2 1e52308
update the application controller
peggles2 a8f1aee
changes made to return proper json error responses
peggles2 6cc1dda
latest changes
peggles2 3bbd31e
update code
peggles2 7f1d382
create the profile and cache the pii
solipet 33eab3e
create the profile creation form correctly
solipet 89f4640
clean up the JWT code, use idv certificate pair
solipet 750a933
update code
peggles2 30b83df
Merge branch 'main' of github.com:18F/identity-idp into lg-6114-perso…
peggles2 0fe9764
changes made to cleanup code
peggles2 c06c5c6
Merge branch 'main' of github.com:18F/identity-idp into lg-6114-perso…
peggles2 0b87d4f
update FormResponse to return a {} if extra_attributes is nil
peggles2 c201eb9
changes made to fix the correct jwt to return user key
peggles2 4928fdc
specs for Api::ProfileCreationForm
solipet ba29220
specs for Api::ProfileCreationForm (for reals)
solipet 91ea768
Merge branch 'main' of github.com:18F/identity-idp into lg-6114-perso…
peggles2 5c44e19
Merge branch 'lg-6114-personal-key' of github.com:18F/identity-idp in…
peggles2 f24ca6d
add rspec tests
peggles2 2b7abf7
cleanup test
peggles2 432732b
cleanup code
peggles2 d3882dd
fix profile creation form spec on recovery key
solipet 57c8ff7
changes made to make it a post instead of a get
peggles2 2aead0d
cleanup lint
peggles2 faf002c
fix some more linter errors
peggles2 3f00de9
Include "personal_key" as alertable key in analytics PiiDetector
aduth 078af46
Revert "Include "personal_key" as alertable key in analytics PiiDetec…
aduth 35b064c
implement/test complete_session
solipet e103006
changes made to fix the code review feedacks
peggles2 4c22483
fix conflicts
peggles2 b2ecfe1
get rid of aliased methods
solipet 000690d
lints
solipet d12fee1
fix linter error
peggles2 37a9df6
Merge branch 'lg-6114-personal-key' of github.com:18F/identity-idp in…
peggles2 f0c8b47
remove parenthesis
peggles2 31c71bc
code review feedback
peggles2 310e6df
changelog: Upcoming Features, Identity Verification, API endpoint for…
peggles2 a4be0be
fix line space
peggles2 4ee9fb0
code review feedback
peggles2 784861f
fix lint error
peggles2 6a0e7b8
add feature flagging
peggles2 609480a
move the personal_key to a dedicated method, encapsulate the JWT in a…
solipet b4fc505
lints
solipet a43e859
convert profile_completion_form to return the personal_key separately…
solipet b8eb235
remove unused custom form response class
solipet 0784d74
Update config/routes.rb
solipet a21836c
Update app/forms/api/profile_creation_form.rb
solipet ddaad6b
remove `gpo_otp` as a method on the form
solipet 65018ce
move the feature flag check from routes.rb to the controller
solipet c1fecba
Merge branch 'main' into lg-6114-personal-key
solipet d5ae4c7
remove unnecessary session usage
solipet 5683aad
default keys for IdV JWTs
solipet 67a3940
guard against small IdV JWT keys in production envs
solipet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.