From 911d7dfaa6cd3938da477ec15a5497a6e7983aa8 Mon Sep 17 00:00:00 2001 From: Liz Townsend <72234279+liztownd@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:09:05 -0500 Subject: [PATCH] BTSSS/Extract Auth (#18382) * separate token service, update and add tests * split services and clients, get tokens from controller * tidying * change return statement, remove token service from claims client * remove mock data option from token service * make base client for shared code * fix funky destructuring * simplify before for claims spec * remove extra commented line * add token_service specific happy path test * consolidate claims client code * move before block to set up for failed call tests * re-delete client file --- .../travel_pay/v0/claims_controller.rb | 11 +- .../app/services/travel_pay/claims_client.rb | 120 +----------------- .../app/services/travel_pay/claims_service.rb | 4 +- .../app/services/travel_pay/token_client.rb | 116 +++++++++++++++++ .../app/services/travel_pay/token_service.rb | 21 +++ .../spec/services/claims_client_spec.rb | 95 +------------- .../spec/services/claims_service_spec.rb | 4 +- .../spec/services/token_client_spec.rb | 102 +++++++++++++++ .../spec/services/token_service_spec.rb | 36 ++++++ 9 files changed, 293 insertions(+), 216 deletions(-) create mode 100644 modules/travel_pay/app/services/travel_pay/token_client.rb create mode 100644 modules/travel_pay/app/services/travel_pay/token_service.rb create mode 100644 modules/travel_pay/spec/services/token_client_spec.rb create mode 100644 modules/travel_pay/spec/services/token_service_spec.rb diff --git a/modules/travel_pay/app/controllers/travel_pay/v0/claims_controller.rb b/modules/travel_pay/app/controllers/travel_pay/v0/claims_controller.rb index 735f26331f9..9ae54f0a0b6 100644 --- a/modules/travel_pay/app/controllers/travel_pay/v0/claims_controller.rb +++ b/modules/travel_pay/app/controllers/travel_pay/v0/claims_controller.rb @@ -5,7 +5,8 @@ module V0 class ClaimsController < ApplicationController def index begin - claims = service.get_claims(@current_user) + token_service.get_tokens(@current_user) => { veis_token:, btsss_token: } + claims = claims_service.get_claims(veis_token, btsss_token) rescue Faraday::Error => e TravelPay::ServiceError.raise_mapped_error(e) end @@ -15,8 +16,12 @@ def index private - def service - @service ||= TravelPay::ClaimsService.new + def claims_service + @claims_service ||= TravelPay::ClaimsService.new + end + + def token_service + @token_service ||= TravelPay::TokenService.new end def common_exception(e) diff --git a/modules/travel_pay/app/services/travel_pay/claims_client.rb b/modules/travel_pay/app/services/travel_pay/claims_client.rb index 8dcaad7c524..c5b29f49646 100644 --- a/modules/travel_pay/app/services/travel_pay/claims_client.rb +++ b/modules/travel_pay/app/services/travel_pay/claims_client.rb @@ -1,133 +1,17 @@ # frozen_string_literal: true require 'securerandom' -require_relative 'base_client' +require_relative './base_client' module TravelPay class ClaimsClient < TravelPay::BaseClient - ## - # HTTP POST call to the VEIS Auth endpoint to get the access token - # - # @return [Faraday::Response] - # - def request_veis_token - auth_url = Settings.travel_pay.veis.auth_url - tenant_id = Settings.travel_pay.veis.tenant_id - - response = connection(server_url: auth_url).post("#{tenant_id}/oauth2/token") do |req| - req.headers[:content_type] = 'application/x-www-form-urlencoded' - req.body = URI.encode_www_form(veis_params) - end - - response.body['access_token'] - end - - ## - # HTTP POST call to the BTSSS token endpoint to get the access token - # - # @return [Faraday::Response] - # - def request_btsss_token(veis_token, sts_token) - btsss_url = Settings.travel_pay.base_url - client_number = Settings.travel_pay.client_number - correlation_id = SecureRandom.uuid - Rails.logger.debug(message: 'Correlation ID', correlation_id:) - - response = connection(server_url: btsss_url).post('api/v1/Auth/access-token') do |req| - req.headers['Authorization'] = "Bearer #{veis_token}" - req.headers['BTSSS-API-Client-Number'] = client_number.to_s - req.headers['X-Correlation-ID'] = correlation_id - req.headers.merge!(claim_headers) - req.body = { authJwt: sts_token } - end - - response.body['data']['accessToken'] - end - ## # HTTP GET call to the BTSSS 'claims' endpoint # API responds with travel pay claims including status # # @return [TravelPay::Claim] # - def get_claims(current_user) - veis_token = request_veis_token - - sts_token = request_sts_token(current_user) - btsss_token = request_btsss_token(veis_token, sts_token) - - request_claims(veis_token, btsss_token) - end - - def request_sts_token(user) - return nil if mock_enabled? - - host_baseurl = build_host_baseurl({ ip_form: false }) - private_key_file = Settings.sign_in.sts_client.key_path - private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file)) - - assertion = build_sts_assertion(user) - jwt = JWT.encode(assertion, private_key, 'RS256') - - # send to sis - response = connection(server_url: host_baseurl).post('/v0/sign_in/token') do |req| - req.params['grant_type'] = 'urn:ietf:params:oauth:grant-type:jwt-bearer' - req.params['assertion'] = jwt - end - - response.body['data']['access_token'] - end - - private - - def build_sts_assertion(user) - service_account_id = Settings.travel_pay.sts.service_account_id - host_baseurl = build_host_baseurl({ ip_form: false }) - audience_baseurl = build_host_baseurl({ ip_form: true }) - scopes = Settings.travel_pay.sts.scope.blank? ? [] : [Settings.travel_pay.sts.scope] - - current_time = Time.now.to_i - jti = SecureRandom.uuid - - { - 'iss' => host_baseurl, - 'sub' => user.email, - 'aud' => "#{audience_baseurl}/v0/sign_in/token", - 'iat' => current_time, - 'exp' => current_time + 300, - 'scopes' => scopes, - 'service_account_id' => service_account_id, - 'jti' => jti, - 'user_attributes' => { 'icn' => user.icn } - } - end - - def build_host_baseurl(config) - env = Settings.vsp_environment - host = Settings.hostname - - if env == 'localhost' - if config[:ip_form] - return 'http://127.0.0.1:3000' - else - return 'http://localhost:3000' - end - end - - "https://#{host}" - end - - def veis_params - { - client_id: Settings.travel_pay.veis.client_id, - client_secret: Settings.travel_pay.veis.client_secret, - client_info: 1, - grant_type: 'client_credentials', - resource: Settings.travel_pay.veis.resource - } - end - - def request_claims(veis_token, btsss_token) + def get_claims(veis_token, btsss_token) btsss_url = Settings.travel_pay.base_url correlation_id = SecureRandom.uuid Rails.logger.debug(message: 'Correlation ID', correlation_id:) diff --git a/modules/travel_pay/app/services/travel_pay/claims_service.rb b/modules/travel_pay/app/services/travel_pay/claims_service.rb index 67467748eab..6729009a616 100644 --- a/modules/travel_pay/app/services/travel_pay/claims_service.rb +++ b/modules/travel_pay/app/services/travel_pay/claims_service.rb @@ -2,8 +2,8 @@ module TravelPay class ClaimsService - def get_claims(current_user) - claims_response = client.get_claims(current_user) + def get_claims(veis_token, btsss_token) + claims_response = client.get_claims(veis_token, btsss_token) symbolized_body = claims_response.body.deep_symbolize_keys { diff --git a/modules/travel_pay/app/services/travel_pay/token_client.rb b/modules/travel_pay/app/services/travel_pay/token_client.rb new file mode 100644 index 00000000000..a608a8667bd --- /dev/null +++ b/modules/travel_pay/app/services/travel_pay/token_client.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'securerandom' +require_relative './base_client' + +module TravelPay + class TokenClient < TravelPay::BaseClient + # HTTP POST call to the VEIS Auth endpoint to get the access token + # + # @return [Faraday::Response] + # + def request_veis_token + auth_url = Settings.travel_pay.veis.auth_url + tenant_id = Settings.travel_pay.veis.tenant_id + + response = connection(server_url: auth_url).post("#{tenant_id}/oauth2/token") do |req| + req.headers[:content_type] = 'application/x-www-form-urlencoded' + req.body = URI.encode_www_form(veis_params) + end + + response.body['access_token'] + end + + ## + # HTTP POST call to the BTSSS token endpoint to get the access token + # + # @return [Faraday::Response] + # + def request_btsss_token(veis_token, user) + sts_token = request_sts_token(user) + + btsss_url = Settings.travel_pay.base_url + client_number = Settings.travel_pay.client_number + correlation_id = SecureRandom.uuid + Rails.logger.debug(message: 'Correlation ID', correlation_id:) + + response = connection(server_url: btsss_url).post('api/v1/Auth/access-token') do |req| + req.headers['Authorization'] = "Bearer #{veis_token}" + req.headers['BTSSS-API-Client-Number'] = client_number.to_s + req.headers['X-Correlation-ID'] = correlation_id + req.headers.merge!(claim_headers) + req.body = { authJwt: sts_token } + end + + response.body['data']['accessToken'] + end + + def request_sts_token(user) + return nil if mock_enabled? + + host_baseurl = build_host_baseurl({ ip_form: false }) + private_key_file = Settings.sign_in.sts_client.key_path + private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file)) + + assertion = build_sts_assertion(user) + jwt = JWT.encode(assertion, private_key, 'RS256') + + # send to sis + response = connection(server_url: host_baseurl).post('/v0/sign_in/token') do |req| + req.params['grant_type'] = 'urn:ietf:params:oauth:grant-type:jwt-bearer' + req.params['assertion'] = jwt + end + + response.body['data']['access_token'] + end + + def build_sts_assertion(user) + service_account_id = Settings.travel_pay.sts.service_account_id + host_baseurl = build_host_baseurl({ ip_form: false }) + audience_baseurl = build_host_baseurl({ ip_form: true }) + scopes = Settings.travel_pay.sts.scope.blank? ? [] : [Settings.travel_pay.sts.scope] + + current_time = Time.now.to_i + jti = SecureRandom.uuid + + { + 'iss' => host_baseurl, + 'sub' => user.email, + 'aud' => "#{audience_baseurl}/v0/sign_in/token", + 'iat' => current_time, + 'exp' => current_time + 300, + 'scopes' => scopes, + 'service_account_id' => service_account_id, + 'jti' => jti, + 'user_attributes' => { 'icn' => user.icn } + } + end + + private + + def veis_params + { + client_id: Settings.travel_pay.veis.client_id, + client_secret: Settings.travel_pay.veis.client_secret, + client_info: 1, + grant_type: 'client_credentials', + resource: Settings.travel_pay.veis.resource + } + end + + def build_host_baseurl(config) + env = Settings.vsp_environment + host = Settings.hostname + + if env == 'localhost' + if config[:ip_form] + return 'http://127.0.0.1:3000' + else + return 'http://localhost:3000' + end + end + + "https://#{host}" + end + end +end diff --git a/modules/travel_pay/app/services/travel_pay/token_service.rb b/modules/travel_pay/app/services/travel_pay/token_service.rb new file mode 100644 index 00000000000..5ad375f723b --- /dev/null +++ b/modules/travel_pay/app/services/travel_pay/token_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module TravelPay + class TokenService + # + # returns a hash containing the veis_token & btsss_token + # + def get_tokens(current_user) + veis_token = token_client.request_veis_token + btsss_token = token_client.request_btsss_token(veis_token, current_user) + + { veis_token:, btsss_token: } + end + + private + + def token_client + TravelPay::TokenClient.new + end + end +end diff --git a/modules/travel_pay/spec/services/claims_client_spec.rb b/modules/travel_pay/spec/services/claims_client_spec.rb index bfad0cdd33d..f0195a4dea8 100644 --- a/modules/travel_pay/spec/services/claims_client_spec.rb +++ b/modules/travel_pay/spec/services/claims_client_spec.rb @@ -35,57 +35,11 @@ end end - context 'request_veis_token' do - it 'returns veis token from proper endpoint' do - tenant_id = Settings.travel_pay.veis.tenant_id - @stubs.post("#{tenant_id}/oauth2/token") do - [ - 200, - { 'Content-Type': 'application/json' }, - '{"access_token": "fake_veis_token"}' - ] - end - client = TravelPay::ClaimsClient.new - token = client.request_veis_token - - expect(token).to eq('fake_veis_token') - @stubs.verify_stubbed_calls - end - end - - context 'request_btsss_token' do - let(:vagov_token) { 'fake_vagov_token' } - let(:json_request_body) { { authJwt: 'fake_vagov_token' }.to_json } - - it 'returns btsss token from proper endpoint' do - @stubs.post('/api/v1/Auth/access-token', json_request_body) do - [ - 200, - { 'Content-Type': 'application/json' }, - '{"data": {"accessToken": "fake_btsss_token"}}' - ] - end - - client = TravelPay::ClaimsClient.new - token = client.request_btsss_token('fake_veis_token', vagov_token) - - expect(token).to eq('fake_btsss_token') - @stubs.verify_stubbed_calls - end - end - context '/claims' do before do - allow_any_instance_of(TravelPay::ClaimsClient) - .to receive(:request_veis_token) - .and_return('veis_token') - allow_any_instance_of(TravelPay::ClaimsClient) - .to receive(:request_sts_token) - .and_return('sts_token') - allow_any_instance_of(TravelPay::ClaimsClient) - .to receive(:request_btsss_token) - .with('veis_token', 'sts_token') - .and_return('btsss_token') + allow_any_instance_of(TravelPay::TokenService) + .to receive(:get_tokens) + .and_return('veis_token', 'btsss_token') end it 'returns response from claims endpoint' do @@ -130,51 +84,10 @@ expected_ids = %w[uuid1 uuid2 uuid3] client = TravelPay::ClaimsClient.new - claims_response = client.get_claims(user) + claims_response = client.get_claims('veis_token', 'btsss_token') actual_claim_ids = claims_response.body['data'].pluck('id') expect(actual_claim_ids).to eq(expected_ids) end end - - context 'request_sts_token' do - let(:assertion) do - { - 'iss' => 'https://www.example.com', - 'sub' => user.email, - 'aud' => 'https://www.example.com/v0/sign_in/token', - 'iat' => 1_634_745_556, - 'exp' => 1_634_745_856, - 'scopes' => [], - 'service_account_id' => nil, - 'jti' => 'c3fa0763-70cb-419a-b3a6-d2563e7b8504', - 'user_attributes' => { 'icn' => '123498767V234859' } - } - end - let(:grant_type) { 'urn:ietf:params:oauth:grant-type:jwt-bearer' } - - before do - Timecop.freeze(Time.zone.parse('2021-10-20T15:59:16Z')) - allow(SecureRandom).to receive(:uuid).and_return('c3fa0763-70cb-419a-b3a6-d2563e7b8504') - end - - after { Timecop.return } - - it 'builds sts assertion and requests sts token' do - private_key_file = Settings.sign_in.sts_client.key_path - private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file)) - jwt = JWT.encode(assertion, private_key, 'RS256') - @stubs.post("http:/v0/sign_in/token?assertion=#{jwt}&grant_type=#{grant_type}") do - [ - 200, - { 'Content-Type': 'application/json' }, - '{"data": {"access_token": "fake_sts_token"}}' - ] - end - client = TravelPay::ClaimsClient.new - sts_token = client.request_sts_token(user) - expect(sts_token).to eq('fake_sts_token') - @stubs.verify_stubbed_calls - end - end end diff --git a/modules/travel_pay/spec/services/claims_service_spec.rb b/modules/travel_pay/spec/services/claims_service_spec.rb index 5e7588be39e..5fad845d35b 100644 --- a/modules/travel_pay/spec/services/claims_service_spec.rb +++ b/modules/travel_pay/spec/services/claims_service_spec.rb @@ -57,7 +57,7 @@ before do allow_any_instance_of(TravelPay::ClaimsClient) .to receive(:get_claims) - .with(user) + .with('veis_token', 'btsss_token') .and_return(claims_response) end @@ -65,7 +65,7 @@ expected_statuses = ['In Progress', 'In Progress', 'Incomplete', 'Claim Submitted'] service = TravelPay::ClaimsService.new - claims = service.get_claims(user) + claims = service.get_claims('veis_token', 'btsss_token') actual_statuses = claims[:data].pluck(:claimStatus) expect(actual_statuses).to match_array(expected_statuses) diff --git a/modules/travel_pay/spec/services/token_client_spec.rb b/modules/travel_pay/spec/services/token_client_spec.rb new file mode 100644 index 00000000000..68326df7150 --- /dev/null +++ b/modules/travel_pay/spec/services/token_client_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe TravelPay::TokenClient do + let(:user) { build(:user) } + + before do + @stubs = Faraday::Adapter::Test::Stubs.new + + conn = Faraday.new do |c| + c.adapter(:test, @stubs) + c.response :json + c.request :json + end + + allow_any_instance_of(TravelPay::TokenClient).to receive(:connection).and_return(conn) + end + + context 'request_veis_token' do + it 'returns veis token from proper endpoint' do + tenant_id = Settings.travel_pay.veis.tenant_id + @stubs.post("#{tenant_id}/oauth2/token") do + [ + 200, + { 'Content-Type': 'application/json' }, + '{"access_token": "fake_veis_token"}' + ] + end + token_client = TravelPay::TokenClient.new + token = token_client.request_veis_token + + expect(token).to eq('fake_veis_token') + @stubs.verify_stubbed_calls + end + end + + context 'request_btsss_token' do + before do + allow_any_instance_of(TravelPay::TokenClient) + .to receive(:request_sts_token) + .and_return('sts_token') + end + + it 'returns btsss token from proper endpoint' do + @stubs.post('api/v1/Auth/access-token') do + [ + 200, + { 'Content-Type': 'application/json' }, + '{"data": {"accessToken": "fake_btsss_token"}}' + ] + end + + token_client = TravelPay::TokenClient.new + token = token_client.request_btsss_token('veis_token', user) + + expect(token).to eq('fake_btsss_token') + @stubs.verify_stubbed_calls + end + end + + context 'request_sts_token' do + let(:assertion) do + { + 'iss' => 'https://www.example.com', + 'sub' => user.email, + 'aud' => 'https://www.example.com/v0/sign_in/token', + 'iat' => 1_634_745_556, + 'exp' => 1_634_745_856, + 'scopes' => [], + 'service_account_id' => nil, + 'jti' => 'c3fa0763-70cb-419a-b3a6-d2563e7b8504', + 'user_attributes' => { 'icn' => '123498767V234859' } + } + end + let(:grant_type) { 'urn:ietf:params:oauth:grant-type:jwt-bearer' } + + before do + Timecop.freeze(Time.zone.parse('2021-10-20T15:59:16Z')) + allow(SecureRandom).to receive(:uuid).and_return('c3fa0763-70cb-419a-b3a6-d2563e7b8504') + end + + after { Timecop.return } + + it 'builds sts assertion and requests sts token' do + private_key_file = Settings.sign_in.sts_client.key_path + private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file)) + jwt = JWT.encode(assertion, private_key, 'RS256') + @stubs.post("http:/v0/sign_in/token?assertion=#{jwt}&grant_type=#{grant_type}") do + [ + 200, + { 'Content-Type': 'application/json' }, + '{"data": {"access_token": "fake_sts_token"}}' + ] + end + token_client = TravelPay::TokenClient.new + sts_token = token_client.request_sts_token(user) + expect(sts_token).to eq('fake_sts_token') + @stubs.verify_stubbed_calls + end + end +end diff --git a/modules/travel_pay/spec/services/token_service_spec.rb b/modules/travel_pay/spec/services/token_service_spec.rb new file mode 100644 index 00000000000..d3404af5069 --- /dev/null +++ b/modules/travel_pay/spec/services/token_service_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe TravelPay::TokenService do + context 'get_tokens' do + let(:user) { build(:user) } + let(:tokens) do + { + veis_token: 'fake_veis_token', + btsss_token: 'fake_btsss_token' + } + end + let(:tokens_response) do + Faraday::Response.new( + body: tokens + ) + end + + context 'get_tokens' do + it 'returns a hash with a veis_token and a btsss_token' do + allow_any_instance_of(TravelPay::TokenClient) + .to receive(:request_veis_token) + .and_return(tokens[:veis_token]) + allow_any_instance_of(TravelPay::TokenClient) + .to receive(:request_btsss_token) + .with(tokens[:veis_token], user) + .and_return(tokens[:btsss_token]) + + service = TravelPay::TokenService.new + response = service.get_tokens(user) + expect(response).to eq(tokens) + end + end + end +end