Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 86 additions & 0 deletions app/services/inherited_proofing/va/service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module InheritedProofing
module Va
# Encapsulates request, response, error handling, validation, etc. for calling
# the VA service to gain PII for a particular user that will be subsequently
# used to proof the user using inherited proofing.
class Service
BASE_URI = IdentityConfig.store.inherited_proofing_va_base_url

attr_reader :auth_code

def initialize(auth_code)
@auth_code = auth_code
end

# Calls the endpoint and returns the decrypted response.
def execute
raise 'The provided auth_code is blank?' if auth_code.blank?

response = request
payload_to_hash decrypt_payload(response)
end

private

def request
connection.get(request_uri) { |req| req.headers = request_headers }
end

def connection
Faraday.new do |conn|
conn.options.timeout = request_timeout
conn.options.read_timeout = request_timeout
conn.options.open_timeout = request_timeout
conn.options.write_timeout = request_timeout
conn.request :instrumentation, name: 'inherited_proofing.va'

# raises errors on 4XX or 5XX responses
conn.response :raise_error
end
end

def request_timeout
@request_timeout ||= IdentityConfig.store.doc_auth_s3_request_timeout
end

def request_uri
@request_uri ||= "#{ URI(BASE_URI) }/inherited_proofing/user_attributes"
end

def request_headers
{ Authorization: "Bearer #{jwt_token}" }
end

def jwt_token
JWT.encode(jwt_payload, private_key, jwt_encryption)
end

def jwt_payload
{ inherited_proofing_auth: auth_code, exp: jwt_expires }
end

def private_key
@private_key ||= AppArtifacts.store.oidc_private_key
end

def jwt_encryption
'RS256'
end

def jwt_expires
1.day.from_now.to_i
end

def decrypt_payload(response)
payload = JSON.parse(response.body)['data']
JWE.decrypt(payload, private_key) if payload
end

def payload_to_hash(decrypted_payload, default: nil)
return default unless decrypted_payload.present?

JSON.parse(decrypted_payload, symbolize_names: true)
end
end
end
end
1 change: 1 addition & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ voice_otp_speech_rate: 'slow'
voip_check: true
voip_block: true
voip_allowed_phones: '[]'
inherited_proofing_va_base_url: 'https://staging-api.va.gov'

development:
aamva_private_key: 123abc
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ def self.build_store(config_map)
config.add(:voip_allowed_phones, type: :json)
config.add(:voip_block, type: :boolean)
config.add(:voip_check, type: :boolean)
config.add(:inherited_proofing_va_base_url, type: :string)

@store = RedactedStruct.new('IdentityConfig', *config.written_env.keys, keyword_init: true).
new(**config.written_env)
Expand Down
60 changes: 60 additions & 0 deletions spec/services/inherited_proofing/va/service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'rails_helper'

RSpec.shared_examples 'an invalid auth code error is raised' do
it 'raises an error' do
expect { subject.execute }.to raise_error 'The provided auth_code is blank?'
end
end

RSpec.describe InheritedProofing::Va::Service do
subject(:service) { described_class.new auth_code }

before do
allow(service).to receive(:private_key).and_return(private_key)
end

let(:auth_code) {}
let(:private_key) { private_key_from_store_or(file_name: 'va_ip.key') }
let(:payload) { { inherited_proofing_auth: auth_code, exp: 1.day.from_now.to_i } }
let(:jwt_token) { JWT.encode(payload, private_key, 'RS256') }
let(:request_uri) {
"#{InheritedProofing::Va::Service::BASE_URI}/inherited_proofing/user_attributes"
}
let(:request_headers) { { Authorization: "Bearer #{jwt_token}" } }

it { respond_to :execute }

it do
expect(service.send(:private_key)).to eq private_key
end

describe '#execute' do
context 'when the auth code is valid' do
let(:auth_code) { 'mocked-auth-code-for-testing' }

it 'makes an authenticated request' do
stub = stub_request(:get, request_uri).
with(headers: request_headers).
to_return(status: 200, body: '{}', headers: {})

service.execute

expect(stub).to have_been_requested.once
end
end

context 'when the auth code is invalid' do
context 'when an empty? string' do
let(:auth_code) { '' }

it_behaves_like 'an invalid auth code error is raised'
end

context 'when an nil?' do
let(:auth_code) { nil }

it_behaves_like 'an invalid auth code error is raised'
end
end
end
end
27 changes: 27 additions & 0 deletions spec/support/private_key_file_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module PrivateKeyFileHelper
# Returns the private key in AppArtifacts.store.oidc_private_key if
# Identity::Hostdata.in_datacenter? or if the private key file does
# not exist; otherwise, the private key from the file is returned.
def private_key_from_store_or(file_name:)
file_name = force_tmp_private_key_file_name file_name: file_name

if Rails.env.test? && !File.exist?(file_name)
puts "WARNING: Private key file '#{file_name}' not found!"
end

if File.exist?(file_name)
OpenSSL::PKey::RSA.new(File.read(file_name))
else
return AppArtifacts.store.oidc_private_key
end
end

# Always ensure we're referencing files in the /tmp/ folder!
def force_tmp_private_key_file_name(file_name:)
"#{Rails.root}/tmp/#{File.basename(file_name)}"
end
end

RSpec.configure do |config|
config.include PrivateKeyFileHelper
end