From d4a5eb4cc3e3e18158e043b47b59b277081d6f49 Mon Sep 17 00:00:00 2001 From: Emily Michaud <59289146+emichaud998@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:26:22 -0500 Subject: [PATCH 1/4] Add SMART authorize and token endpoints so that clients can connect via SMART. Update wait test to use client id so that it can be used both for the SMART requests and resource api requests --- .../client/v2.0.0/c4bb_client_test_suite.rb | 6 +- .../coverage_claims_data_request_test.rb | 1 - .../eob_inpatient_claims_data_request_test.rb | 1 - .../eob_oral_claims_data_request_test.rb | 1 - ...eob_outpatient_claims_data_request_test.rb | 1 - .../eob_pharmacy_claims_data_request_test.rb | 1 - ...b_professional_claims_data_request_test.rb | 1 - .../organization_claims_data_request_test.rb | 1 - .../patient_claims_data_request_test.rb | 1 - .../practitioner_claims_data_request_test.rb | 1 - .../relatedperson_claims_data_request_test.rb | 1 - .../v2.0.0/endpoints/authorize_endpoint.rb | 32 +++++ .../client/v2.0.0/endpoints/token_endpoint.rb | 127 ++++++++++++++++-- .../client/v2.0.0/initial_wait_test.rb | 14 +- ...son => mock_capability_statement.json.erb} | 28 ++++ .../client/v2.0.0/mock_server.rb | 46 +++++-- .../coverage_required_searches.rb | 1 - .../eob_required_searches.rb | 1 - .../organization_required_searches.rb | 1 - .../patient_required_searches.rb | 1 - .../practitioner_required_searches.rb | 1 - .../relatedperson_required_searches.rb | 1 - .../client/v2.0.0/tags.rb | 2 + .../client/v2.0.0/urls.rb | 10 ++ .../c4bb_client_eob_required_searches_spec.rb | 6 +- .../c4bb_client_initial_wait_test_spec.rb | 54 ++++---- ...nt_submit_claims_data_request_test_spec.rb | 8 +- 27 files changed, 273 insertions(+), 76 deletions(-) create mode 100644 lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/authorize_endpoint.rb rename lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/{mock_capability_statement.json => mock_capability_statement.json.erb} (95%) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb index 78160ea9..d36e1ed8 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb @@ -1,6 +1,7 @@ require 'inferno/dsl/oauth_credentials' require_relative 'endpoints/resource_api_endpoint' require_relative 'endpoints/token_endpoint' +require_relative 'endpoints/authorize_endpoint' require_relative 'endpoints/next_page_endpoint' require_relative 'endpoints/resource_id_endpoint' @@ -77,7 +78,9 @@ def self.test_resumes?(test) end end - suite_endpoint :post, TOKEN_PATH, TokenEndpoint + suite_endpoint :post, TOKEN_PATH, MockAuthorization::TokenEndpoint + suite_endpoint :get, AUTH_PATH, MockAuthorization::AuthorizeEndpoint + suite_endpoint :post, AUTH_PATH, MockAuthorization::AuthorizeEndpoint suite_endpoint :get, PATIENT_PATH, ResourceAPIEndpoint @@ -100,6 +103,7 @@ def self.test_resumes?(test) end route(:get, METADATA_PATH, get_metadata) + route(:get, SMART_CONFIG_PATH, carin_smart_config) group do run_as_group diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/coverage_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/coverage_claims_data_request_test.rb index dc49dd34..f7376dd2 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/coverage_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/coverage_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientCoverageSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is a Coverage resource that conforms to the CARIN for Blue Button [Coverage profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-Coverage.html). ) - input :access_token run do resources = previous_resource_requests(:Coverage) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_inpatient_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_inpatient_claims_data_request_test.rb index e9eb7618..9104edbc 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_inpatient_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_inpatient_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientEOBInpatientSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is an ExplanationOfBenefit resource that conforms to the CARIN for Blue Button [Outpatient Institutional ExplanationOfBenefit profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-ExplanationOfBenefit-Outpatient-Institutional.html). ) - input :access_token run do resources = previous_resource_requests(:ExplanationOfBenefit_Inpatient_Institutional) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_oral_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_oral_claims_data_request_test.rb index cb77037a..44d45c42 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_oral_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_oral_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientEOBOralSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is an ExplanationOfBenefit resource that conforms to the CARIN for Blue Button [Oral ExplanationOfBenefit profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-ExplanationOfBenefit-Oral.html). ) - input :access_token run do resources = previous_resource_requests(:ExplanationOfBenefit_Oral) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_outpatient_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_outpatient_claims_data_request_test.rb index 32b01919..2e9cd899 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_outpatient_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_outpatient_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientEOBOutpatientSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is an ExplanationOfBenefit resource that conforms to the CARIN for Blue Button [Outpatient Institutional ExplanationOfBenefit profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-ExplanationOfBenefit-Outpatient-Institutional.html). ) - input :access_token run do resources = previous_resource_requests(:ExplanationOfBenefit_Outpatient_Institutional) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_pharmacy_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_pharmacy_claims_data_request_test.rb index 284b0521..396d0d52 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_pharmacy_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_pharmacy_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientEOBPharmacySubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is an ExplanationOfBenefit resource that conforms to the CARIN for Blue Button [Pharmacy ExplanationOfBenefit profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-ExplanationOfBenefit-Pharmacy.html). ) - input :access_token run do resources = previous_resource_requests(:ExplanationOfBenefit_Pharmacy) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_professional_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_professional_claims_data_request_test.rb index a3387df4..8a5eede4 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_professional_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/eob_professional_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientEOBProfessionalSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is an ExplanationOfBenefit resource that conforms to the CARIN for Blue Button [Professional NonClinician ExplanationOfBenefit profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-ExplanationOfBenefit-Professional-NonClinician.html). ) - input :access_token run do resources = previous_resource_requests(:ExplanationOfBenefit_Professional_NonClinician) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/organization_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/organization_claims_data_request_test.rb index c66908d0..9cab9ff7 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/organization_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/organization_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientOrganizationSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is an Organization resource that conforms to the CARIN for Blue Button [Organization profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-Organization.html). ) - input :access_token run do resources = previous_resource_requests(:Organization) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/patient_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/patient_claims_data_request_test.rb index f6be7c84..a4c4e808 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/patient_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/patient_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientPatientSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is a Patient resource that conforms to the CARIN for Blue Button [Patient profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-Patient.html). ) - input :access_token run do resources = previous_resource_requests(:Patient) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/practitioner_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/practitioner_claims_data_request_test.rb index d3682d02..9e742e61 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/practitioner_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/practitioner_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientPractitionerSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is a Practitioner resource that conforms to the CARIN for Blue Button [Practitioner profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-Practitioner.html). ) - input :access_token run do resources = previous_resource_requests(:Practitioner) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/relatedperson_claims_data_request_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/relatedperson_claims_data_request_test.rb index e765dd82..be1ae5cf 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/relatedperson_claims_data_request_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/claim_data_request_tests/relatedperson_claims_data_request_test.rb @@ -12,7 +12,6 @@ class C4BBClientRelatedPersonSubmitClaimsDataRequestTest < Inferno::Test This test verifies that an instance returned by requests made by the client is a RelatedPerson resource that conforms to the CARIN for Blue Button [RelatedPerson profile](https://hl7.org/fhir/us/carin-bb/STU2/StructureDefinition-C4BB-RelatedPerson.html). ) - input :access_token run do resources = previous_resource_requests(:RelatedPerson) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/authorize_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/authorize_endpoint.rb new file mode 100644 index 00000000..b9830190 --- /dev/null +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/authorize_endpoint.rb @@ -0,0 +1,32 @@ +module CarinForBlueButtonTestKit + module MockAuthorization + class AuthorizeEndpoint < Inferno::DSL::SuiteEndpoint + def test_run_identifier + request.params[:client_id] + end + + def tags + [AUTHORIZE_TAG] + end + + def make_response + if request.params[:redirect_uri].present? + redirect_uri = "#{request.params[:redirect_uri]}?" \ + "code=#{SecureRandom.hex}&" \ + "state=#{request.params[:state]}" + response.status = 302 + response.headers['Location'] = redirect_uri + else + response.status = 400 + response.format = 'application/fhir+json' + response.body = FHIR::OperationOutcome.new( + issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'required', + details: FHIR::CodeableConcept.new( + text: 'No redirect_uri provided' + )) + ).to_json + end + end + end + end +end diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb index df2bd2bf..7fe092bd 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb @@ -1,24 +1,125 @@ require_relative '../tags' +require_relative '../urls' require_relative '../mock_server' module CarinForBlueButtonTestKit - class TokenEndpoint < Inferno::DSL::SuiteEndpoint - include CarinForBlueButtonTestKit::MockServer + module MockAuthorization + AUTHORIZED_PRACTITIONER_ID = 'c4bb-Practitioner'.freeze # Must exist on the FHIR_REFERENCE_SERVER (env var) - def test_run_identifier - extract_client_id(request) - end + class TokenEndpoint < Inferno::DSL::SuiteEndpoint + include CarinForBlueButtonTestKit::MockServer - def make_response - token_response(request) - end + def test_run_identifier + extract_client_id + end - def tags - [AUTH_TAG] - end + def tags + [TOKEN_TAG] + end + + def make_response + client_id = extract_client_id + access_token = client_id + granted_scopes = SUPPORTED_SCOPES & requested_scopes + + response_hash = { access_token:, scope: granted_scopes.join(' '), token_type: 'bearer', + expires_in: 3600 } + + if granted_scopes.include?('openid') + response_hash.merge!(id_token: create_id_token(client_id, + fhir_user: granted_scopes.include?('fhirUser'))) + end + + # fhir_context_input = find_test_input("#{input_group_prefix}_smart_fhir_context") + # begin + # fhir_context = JSON.parse(fhir_context_input) + # rescue StandardError + # fhir_context = nil + # end + # response_hash.merge!(fhirContext: fhir_context) if fhir_context + + # smart_patient_input = find_test_input("#{input_group_prefix}_smart_patient_id") + # response_hash.merge!(patient: smart_patient_input) if smart_patient_input + + response_hash.merge!(patient: '888') + + response.body = response_hash.to_json + response.headers['Cache-Control'] = 'no-store' + response.headers['Pragma'] = 'no-cache' + response.headers['Access-Control-Allow-Origin'] = '*' + response.status = 200 + end + + private + + def extract_client_id + # Public client || confidential client asymmetric || confidential client symmetric + request.params[:client_id] || extract_client_id_from_client_assertion || extract_client_id_from_basic_auth + end + + def extract_client_id_from_client_assertion + encoded_jwt = request.params[:client_assertion] + return unless encoded_jwt.present? + + jwt_payload = + begin + JWT.decode(encoded_jwt, nil, false)&.first # skip signature verification + rescue StandardError + nil + end + + jwt_payload['iss'] || jwt_payload['sub'] if jwt_payload.present? + end + + def input_group_prefix + if test.id.include?('static') + 'static' + elsif test.id.include?('adaptive') + 'adaptive' + else + 'resp' + end + end + + def find_test_input(input_name) + JSON.parse(result.input_json)&.find { |input| input['name'] == input_name }&.dig('value') + end + + def extract_client_id_from_basic_auth + encoded_credentials = request.headers['authorization']&.delete_prefix('Basic ') + return unless encoded_credentials.present? + + decoded_credentials = Base64.decode64(encoded_credentials) + decoded_credentials&.split(':')&.first + end + + def requested_scopes + auth_request = requests_repo.tagged_requests(result.test_session_id, [AUTHORIZE_TAG]).last + return [] unless auth_request + + auth_params = if auth_request.verb.downcase == 'get' + auth_request.query_parameters + else + URI.decode_www_form(auth_request.request_body)&.to_h + end + scope_str = auth_params&.dig('scope') + scope_str ? URI.decode_www_form_component(scope_str).split : [] + end + + def create_id_token(client_id, fhir_user: false) + # No point in mocking an identity provider, just always use known Practitioner as the authorized user + suite_fhir_base_url = request.url.split(TOKEN_PATH).first + BASE_FHIR_PATH + id_token_hash = { + iss: suite_fhir_base_url, + sub: AUTHORIZED_PRACTITIONER_ID, + aud: client_id, + exp: Time.now.to_i + (24 * 60 * 60), # 24 hrs + iat: Time.now.to_i + } + id_token_hash.merge!(fhirUser: "#{suite_fhir_base_url}/Practitioner/#{AUTHORIZED_PRACTITIONER_ID}") if fhir_user - def update_result - results_repo.update(result.id, result: 'pass') unless test.config.options[:accepts_multiple_requests] + JWT.encode(id_token_hash, RSA_PRIVATE_KEY, 'RS256') + end end end end diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb index 0e02b0f8..68ede6de 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb @@ -8,14 +8,20 @@ class C4BBClientInitialWaitTest < Inferno::Test description %( This test will receive claims data requests and search requests until the user confirms they are done. ) - input :access_token + input :client_id, + title: 'Client ID', + description: %( + Enter the client ID you will use to connect to this CARIN server via SMART. This client ID will be sent + back as the access token to send requests to this server. + ) + config options: { accepts_multiple_requests: true } run do wait( - identifier: access_token, + identifier: client_id, message: %( - Access Token: #{access_token} \n + Access Token: #{client_id} \n Submit CARIN requests via the following method: * Single Resource API: `#{submit_url}?:search_params`, with `:endpoint` replaced with the endpoint you want to reach and `:search_params` replaced with the search parameters for the request. @@ -67,7 +73,7 @@ class C4BBClientInitialWaitTest < Inferno::Test * ExplanationOfBenefit:payee * ExplanationOfBenefit:* - [Click here](#{resume_claims_data_url}?token=#{access_token}) when done. + [Click here](#{resume_claims_data_url}?token=#{client_id}) when done. ), timeout: 900 ) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json b/lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json.erb similarity index 95% rename from lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json rename to lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json.erb index f9c8ac80..7fe48c0e 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json.erb @@ -23,6 +23,34 @@ { "mode": "server", "documentation": "The C4BB Server **SHALL**:\n\n1. Support all profiles defined in this Implementation Guide..\n2. Implement the RESTful behavior according to the FHIR specification.\n3. Return the following response classes:\n - (Status 400): invalid parameter\n - (Status 401/4xx): unauthorized request\n - (Status 403): insufficient scope\n - (Status 404): unknown resource\n - (Status 410): deleted resource.\n4. Support json source formats for all CARIN-BB interactions.\n5. Identify the CARIN-BB profiles supported as part of the FHIR `meta.profile` attribute for each instance.\n6. Support the searchParameters on each profile individually and in combination.\n\nThe C4BB Server **SHOULD**:\n\n1. Support xml source formats for all C4BB interactions.\n", + "security": { + "extension": [ + { + "url": "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris", + "extension": [ + { + "url": "token", + "valueUri": "<%= Inferno::Application['base_url'] %>/custom/c4bb_v200_client/mock_auth/token" + }, + { + "url": "authorize", + "valueUri": "<%= Inferno::Application['base_url'] %>/custom/c4bb_v200_client/mock_auth/authorization" + } + ] + } + ], + "service": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/restful-security-service", + "code": "SMART-on-FHIR" + } + ], + "text": "OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)" + } + ] + }, "resource": [ { "extension": [ diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb index 4d5122c0..edd1c060 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb @@ -2,7 +2,6 @@ require_relative 'urls' require_relative 'collection' require_relative 'client_validation_test' -# require_relative 'metadata/mock_capability_statement' module CarinForBlueButtonTestKit # Serve responses to CARIN requests @@ -13,6 +12,10 @@ module CarinForBlueButtonTestKit module MockServer include URLs + SUPPORTED_SCOPES = ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access', 'openid', 'fhirUser'].freeze + RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.generate(2048) + RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key + def server_proxy @server_proxy ||= Faraday.new( url: ENV.fetch('FHIR_REFERENCE_SERVER'), @@ -25,12 +28,6 @@ def server_proxy end end - def token_response(request, _test = nil, _test_result = nil) - # Placeholder for a more complete mock token endpoint - response.body = { access_token: SecureRandom.hex, token_type: 'bearer', expires_in: 300 }.to_json - response.status = 200 - end - def error_response_resource(request) server_response = server_proxy.get('Patient', { _id: 888 }) response_resource = FHIR.from_contents(server_response.body) @@ -97,9 +94,42 @@ def read_next_page(request, test = nil, test_result = nil) end def get_metadata + erb_template = ERB.new( + File.read( + 'lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json.erb' + ) + ) + capability_statement = JSON.parse(erb_template.result).to_json + proc { [200, { 'Content-Type' => 'application/fhir+json;charset=utf-8' }, - [File.read('lib/carin_for_blue_button_test_kit/client/v2.0.0/metadata/mock_capability_statement.json')]] + [capability_statement]] + } + end + + def carin_smart_config + response_body = + { + authorization_endpoint: authorization_url, + token_endpoint: token_url, + token_endpoint_auth_methods_supported: ['private_key_jwt'], + token_endpoint_auth_signing_alg_values_supported: ['RS256'], + grant_types_supported: ['authorization_code'], + scopes_supported: SUPPORTED_SCOPES, + response_types_supported: ['code'], + code_challenge_methods_supported: ['S256'], + capabilities: [ + 'launch-ehr', + 'permission-patient', + 'permission-user', + 'client-public', + 'client-confidential-symmetric', + 'client-confidential-asymmetric' + ] + }.to_json + + proc { + [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]] } end diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/coverage_required_searches.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/coverage_required_searches.rb index d972d9a3..cadb63f6 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/coverage_required_searches.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/coverage_required_searches.rb @@ -17,7 +17,6 @@ class C4BBClientCoverageRequiredSearches < Inferno::Test * _id * patient ) - input :access_token verifies_requirements 'hl7.fhir.us.carin-bb_2.0.0@14' diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/eob_required_searches.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/eob_required_searches.rb index 0c2304f6..20da4cf0 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/eob_required_searches.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/eob_required_searches.rb @@ -31,7 +31,6 @@ class C4BBClientEOBRequiredSearches < Inferno::Test * ExplanationOfBenefit:payee * ExplanationOfBenefit:* ) - input :access_token verifies_requirements 'hl7.fhir.us.carin-bb_2.0.0@14' diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/organization_required_searches.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/organization_required_searches.rb index d4a5b6de..60700558 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/organization_required_searches.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/organization_required_searches.rb @@ -18,7 +18,6 @@ class C4BBClientOrganizationRequiredSearches < Inferno::Test * name * address ) - input :access_token verifies_requirements 'hl7.fhir.us.carin-bb_2.0.0@14' diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/patient_required_searches.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/patient_required_searches.rb index dfdf3ed0..9efef777 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/patient_required_searches.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/patient_required_searches.rb @@ -26,7 +26,6 @@ class C4BBClientPatientRequiredSearches < Inferno::Test * birthdate+name * gender+name ) - input :access_token verifies_requirements 'hl7.fhir.us.carin-bb_2.0.0@14' diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/practitioner_required_searches.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/practitioner_required_searches.rb index a62a6d19..1463d610 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/practitioner_required_searches.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/practitioner_required_searches.rb @@ -18,7 +18,6 @@ class C4BBClientPractitionerRequiredSearches < Inferno::Test * name * identifier ) - input :access_token verifies_requirements 'hl7.fhir.us.carin-bb_2.0.0@14' diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/relatedperson_required_searches.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/relatedperson_required_searches.rb index a8f9db9e..bf966671 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/relatedperson_required_searches.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/required_searches_tests/relatedperson_required_searches.rb @@ -16,7 +16,6 @@ class C4BBClientRelatedPersonRequiredSearches < Inferno::Test Capability Statements: * _id ) - input :access_token verifies_requirements 'hl7.fhir.us.carin-bb_2.0.0@14' diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/tags.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/tags.rb index 9f553c15..747b1cfb 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/tags.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/tags.rb @@ -4,4 +4,6 @@ module CarinForBlueButtonTestKit AUTH_TAG = 'carin_auth' RESOURCE_API_TAG = 'carin_resource_api' RESOURCE_ID_TAG = 'carin_resource_id' + AUTHORIZE_TAG = 'carin_smart_app_authorize' + TOKEN_TAG = 'carin_smart_app_token' end diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb index b322bce8..fa4d010f 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb @@ -1,5 +1,7 @@ module CarinForBlueButtonTestKit TOKEN_PATH = '/mock_auth/token' + AUTH_PATH = '/mock_auth/authorization' + SMART_CONFIG_PATH = '/.well-known/smart-configuration' PATIENT_PATH = '/fhir/Patient' RESOURCE_API_PATH = '/fhir/:endpoint' RESOURCE_ID_PATH = '/fhir/:endpoint/:id' @@ -18,6 +20,14 @@ def token_url @token_url ||= base_url + TOKEN_PATH end + def authorization_url + @authorization_url ||= base_url + AUTH_PATH + end + + def smart_configuration_url + @smart_configuration_url ||= base_url + SMART_CONFIG_PATH + end + def base_fhir_url @base_fhir_url ||= base_url + BASE_FHIR_PATH end diff --git a/spec/carin_for_blue_button/c4bb_client/c4bb_client_eob_required_searches_spec.rb b/spec/carin_for_blue_button/c4bb_client/c4bb_client_eob_required_searches_spec.rb index 8c3758ba..ca3f3f2b 100644 --- a/spec/carin_for_blue_button/c4bb_client/c4bb_client_eob_required_searches_spec.rb +++ b/spec/carin_for_blue_button/c4bb_client/c4bb_client_eob_required_searches_spec.rb @@ -183,14 +183,14 @@ def run(runnable, inputs = {}) make_all_eob_search_requests(eob_required_searches) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('pass') end it 'skips if no EOB search requests were made' do allow(test).to receive(:suite).and_return(suite) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('skip') expect(result.result_message).to eq('No search requests made for Explanation of Benefit resource') end @@ -201,7 +201,7 @@ def run(runnable, inputs = {}) eob_required_searches.pop make_all_eob_search_requests(eob_required_searches) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('fail') expect(result.result_message).to match(/_id/) end diff --git a/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb b/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb index 4c5fc959..47f3bb16 100644 --- a/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb +++ b/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb @@ -26,10 +26,10 @@ let(:server_patient_api_request) { "#{fhir_server}#{patient_endpoint}" } let(:server_eob_include_search) { "#{fhir_server}#{eob_include_search_endpoint}" } - let(:access_token) { 'SAMPLE_TOKEN' } + let(:client_id) { 'SAMPLE_TOKEN' } let(:resume_pass_url) do - "#{Inferno::Application['base_url']}/custom/c4bb_v200_client/resume_claims_data?token=#{access_token}" + "#{Inferno::Application['base_url']}/custom/c4bb_v200_client/resume_claims_data?token=#{client_id}" end let(:c4bb_eob_include_bundle) do @@ -78,16 +78,16 @@ def run(runnable, inputs = {}) allow(test).to receive_messages(suite:, parent: suite) patient_fhir_api_request = stub_request(:get, server_patient_api_request) - .with( - headers: { 'Authorization' => "Bearer #{access_token}" } - ) - .to_return(status: 200, body: c4bb_patient_search_bundle.to_json) + .with( + headers: { 'Authorization' => "Bearer #{client_id}" } + ) + .to_return(status: 200, body: c4bb_patient_search_bundle.to_json) - result = run(test, access_token:) + result = run(test, client_id:) expect(result.result).to eq('wait') - header 'Authorization', "Bearer #{access_token}" + header 'Authorization', "Bearer #{client_id}" get(patient_api_request) expect(last_response).to be_ok @@ -106,16 +106,16 @@ def run(runnable, inputs = {}) allow(test).to receive_messages(suite:, parent: suite) eob_fhir_include_search = stub_request(:get, server_eob_include_search) - .with( - headers: { 'Authorization' => "Bearer #{access_token}" } - ) - .to_return(status: 200, body: c4bb_eob_include_bundle.to_json) + .with( + headers: { 'Authorization' => "Bearer #{client_id}" } + ) + .to_return(status: 200, body: c4bb_eob_include_bundle.to_json) - result = run(test, access_token:) + result = run(test, client_id:) expect(result.result).to eq('wait') - header 'Authorization', "Bearer #{access_token}" + header 'Authorization', "Bearer #{client_id}" get(eob_include_search) expect(last_response).to be_ok @@ -150,7 +150,7 @@ def run(runnable, inputs = {}) it 'Responds 500 if Bearer token is incorrect' do allow(test).to receive_messages(suite:, parent: suite) - result = run(test, access_token:) + result = run(test, client_id:) header 'Authorization', 'Bearer WRONG_TOKEN' get(patient_api_request) @@ -170,14 +170,14 @@ def run(runnable, inputs = {}) allow(test).to receive_messages(suite:, parent: suite) patient_fhir_api_request = stub_request(:get, server_patient_api_request) - .with( - headers: { 'Authorization' => "Bearer #{access_token}" } - ) - .to_return(status: 200, body: c4bb_patient_search_bundle.to_json) + .with( + headers: { 'Authorization' => "Bearer #{client_id}" } + ) + .to_return(status: 200, body: c4bb_patient_search_bundle.to_json) - result = run(test, access_token:) + result = run(test, client_id:) - header 'Authorization', "Bearer #{access_token}" + header 'Authorization', "Bearer #{client_id}" get(invalid_patient_api_request) expect(last_response.status).to eq(403) @@ -199,16 +199,16 @@ def run(runnable, inputs = {}) allow(test).to receive_messages(suite:, parent: suite) patient_fhir_api_request = stub_request(:get, server_patient_api_request) - .with( - headers: { 'Authorization' => "Bearer #{access_token}" } - ) - .to_return(status: 404, body: c4bb_patient_search_bundle.to_json) + .with( + headers: { 'Authorization' => "Bearer #{client_id}" } + ) + .to_return(status: 404, body: c4bb_patient_search_bundle.to_json) - result = run(test, access_token:) + result = run(test, client_id:) expect(result.result).to eq('wait') - header 'Authorization', "Bearer #{access_token}" + header 'Authorization', "Bearer #{client_id}" get(patient_api_request) expect(last_response.status).to eq(404) diff --git a/spec/carin_for_blue_button/c4bb_client/c4bb_client_patient_submit_claims_data_request_test_spec.rb b/spec/carin_for_blue_button/c4bb_client/c4bb_client_patient_submit_claims_data_request_test_spec.rb index c35d36db..f48a450b 100644 --- a/spec/carin_for_blue_button/c4bb_client/c4bb_client_patient_submit_claims_data_request_test_spec.rb +++ b/spec/carin_for_blue_button/c4bb_client/c4bb_client_patient_submit_claims_data_request_test_spec.rb @@ -105,7 +105,7 @@ def run(runnable, inputs = {}) create_fhir_api_request(body: JSON.parse(c4bb_patient_search_bundle.to_json)) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('pass') end @@ -115,7 +115,7 @@ def run(runnable, inputs = {}) create_fhir_api_request(url: eob_include_search, body: JSON.parse(c4bb_eob_include_bundle.to_json), tags: eob_include_search_tags) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('pass') end @@ -125,7 +125,7 @@ def run(runnable, inputs = {}) c4bb_patient_search_bundle.entry.first.resource.id = 999 create_fhir_api_request(body: JSON.parse(c4bb_patient_search_bundle.to_json)) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('fail') expect(result.result_message).to eq('Unable to find expected resource: 888') end @@ -139,7 +139,7 @@ def run(runnable, inputs = {}) create_fhir_api_request(url: eob_include_search, body: JSON.parse(c4bb_eob_include_bundle.to_json), tags: eob_include_search_tags - ['Patient']) - result = run(test, access_token:) + result = run(test) expect(result.result).to eq('skip') expect(result.result_message).to eq('No requests made for Patient resources') end From ae3fcecf0bfb89f3b92a4a241bae341de9a8aa49 Mon Sep 17 00:00:00 2001 From: Emily Michaud <59289146+emichaud998@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:05:53 -0500 Subject: [PATCH 2/4] Remove commented out code in token_endpoint file --- .../client/v2.0.0/endpoints/token_endpoint.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb index 7fe092bd..b0c346da 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb @@ -30,17 +30,6 @@ def make_response fhir_user: granted_scopes.include?('fhirUser'))) end - # fhir_context_input = find_test_input("#{input_group_prefix}_smart_fhir_context") - # begin - # fhir_context = JSON.parse(fhir_context_input) - # rescue StandardError - # fhir_context = nil - # end - # response_hash.merge!(fhirContext: fhir_context) if fhir_context - - # smart_patient_input = find_test_input("#{input_group_prefix}_smart_patient_id") - # response_hash.merge!(patient: smart_patient_input) if smart_patient_input - response_hash.merge!(patient: '888') response.body = response_hash.to_json From e250aaef8bcd027dad43e26676c387af277661de Mon Sep 17 00:00:00 2001 From: Emily Michaud <59289146+emichaud998@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:18:53 -0500 Subject: [PATCH 3/4] Update preset with client ID input and update description and README with new SMART workflow information --- README.md | 11 +++++------ config/presets/carin_cpcds_client_ri.json | 4 ++-- ...for_blue_button_v2.0.0_client_suite_description.md | 8 +++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8c486b6f..1453c00e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ requests needed to pass all of the tests. Note that some requests within the col To run the client tests against the Postman collection: 1. Start an Inferno session of the CARIN for Blue Button Client test suite. -3. Click the "Run All Tests" button in the upper right and type in "SAMPLE_TOKEN" for the `access_token` input in the dialog that appears. +3. Click the "Run All Tests" button in the upper right and type in "SAMPLE_TOKEN" for the `Client ID` input in the dialog that appears. 4. Click the "Submit" button. The simulated server will then be waiting for an interaction. 4. Open Postman and import the `C4BB Client Search Tests` Postman collection. 5. Send each of the requests listed under the `C4BB Client Search Tests` Postman collection and ensure a @@ -55,8 +55,7 @@ To run the client tests against the Postman collection: ### CPCDS Reference Implementation Client To try out these tests without a Carin for Blue Button client implementation, you may -run them using the [CPCDS Reference Implementation Client](https://github.com/carin-alliance/cpcds-client-ri). Use this -[forked repository](https://github.com/emichaud998/cpcds-client-ri), which been adjusted to work with this test kit. +run them using the [CPCDS Reference Implementation Client](https://github.com/carin-alliance/cpcds-client-ri). 1. Follow the instructions listed [here](https://github.com/carin-alliance/cpcds-client-ri?tab=readme-ov-file#running-app-locally) to get the CPCDS Reference Implementation Client running locally. @@ -70,9 +69,9 @@ To run the client tests against the CPCDS Reference Implementation Client: 3. Click the "Wait for Claims Data and Search Requests" test group in the left navigation sidebar. 4. Click the "Run Tests" button in the upper right and click the "Submit" button in the dialog that appears. The simulated server will then be waiting for an interaction. -5. Navigate to `localhost:3000`. Enter the Carin Client Suite FHIR endpoint URL, and then type the Carin patient id, `888`, - into the client secret and client id fields. Hit the "Connect" button. -6. On the next page, hit the "Display" button to make a request to the Carin Client Suite . +5. Navigate to `localhost:3000`. Enter the Carin Client Suite FHIR endpoint URL, and then type `SAMPLE_CLIENT_ID` + into the client ID field and `SAMPLE_CLIENT_SECRET` into the client secret field. Hit the "Connect" button. +6. This will connect the client with the test suite via SMART and the client will automatically make a request to the test suite. 7. In the Inferno Client Suite, click the "Click here" link in the wait dialog to signal the client has finished submitting requests. 8. Navigate to each Carin for Blue Button Profile test group in the left navigation sidebar, and for each test group hit the run icon next all the tests listed except for the last required search parameters test diff --git a/config/presets/carin_cpcds_client_ri.json b/config/presets/carin_cpcds_client_ri.json index 001371ab..7380060c 100644 --- a/config/presets/carin_cpcds_client_ri.json +++ b/config/presets/carin_cpcds_client_ri.json @@ -4,9 +4,9 @@ "test_suite_id": "c4bb_v200_client", "inputs": [ { - "name": "access_token", + "name": "client_id", "type": "text", - "value": "SAMPLE_TOKEN" + "value": "SAMPLE_CLIENT_ID" } ] } diff --git a/lib/carin_for_blue_button_test_kit/docs/carin_for_blue_button_v2.0.0_client_suite_description.md b/lib/carin_for_blue_button_test_kit/docs/carin_for_blue_button_v2.0.0_client_suite_description.md index f24c86c4..84a06972 100644 --- a/lib/carin_for_blue_button_test_kit/docs/carin_for_blue_button_v2.0.0_client_suite_description.md +++ b/lib/carin_for_blue_button_test_kit/docs/carin_for_blue_button_v2.0.0_client_suite_description.md @@ -40,7 +40,8 @@ During execution, Inferno will wait for the client under test to issue requests The following input must be provided by the tester to execute any tests in this suite: -1. *Access Token*: A `Bearer` token that the client under test will send in the +1. *Client ID*: A Client ID that the client under test will use to connect to the test suite via SMART. + This client ID will be sent back to the client as the `Bearer` token that the client under test will send in the `Authorization` header of HTTP requests made against Inferno. Inferno uses the value to identify incoming requests that belong to the testing session. @@ -63,7 +64,7 @@ requests needed to pass all of the tests. Note that some requests within the col To run the client tests against the Postman collection: 1. Start an Inferno session of the CARIN for Blue Button Client test suite. -3. Click the "Run All Tests" button in the upper right and type in "SAMPLE_TOKEN" for the `access_token` input in the dialog that appears. +3. Click the "Run All Tests" button in the upper right and type in "SAMPLE_TOKEN" for the `Client ID` input in the dialog that appears. 4. Click the "Submit" button. The simulated server will then be waiting for an interaction. 4. Open Postman and import the `C4BB Client Search Tests` Postman collection. 5. Send each of the requests listed under the `C4BB Client Search Tests` Postman collection and ensure a @@ -78,7 +79,7 @@ to use the same token, they may interfere with each other. To prevent concurrent of these sample executions from disrupting your session it is recommended, but not required, to: 1. Update the Authorization tab of the C4BB Client Search Tests collection in Postman to a random value -2. When starting the tests, provide the same value in the access_token input. +2. When starting the tests, provide the same value in the `Client ID` input. ## Current Limitations @@ -91,3 +92,4 @@ Specific current limitations to highlight include: - Inferno's simulated CARIN server does not support all required search parameters on the ExplanationOfBenefit resource, including service-date, service-start-date, billable-period-start, type, and _include=ExplanationOfBenefit:insurer. Inferno recognizes searches made using those parameters and will give the client credit for having performed them, but will always return an OperationOutcome indicating failure. - Testers must manually configure their client system to connect to a specific target patient and ingest specific curated sample CARIN data. Future versions of the tests may allow more flexibility in the patient identity and the associated data. - Testers must attest to their system's ability to process and retain all received information. Currently, this is implemented as a single test. Future versions of the tests may split this test out into different attestations per profile or other more fine-grained organization. + - This test kit contains basic SMART App Launch capabilities that may not be complete. In particular, refresh tokens are not currently supported and scopes are not precise. To provide feedback and input on the design of this feature and help us prioritize improvements, submit a ticket [here](https://github.com/inferno-framework/carin-for-blue-button-test-kit/issues). \ No newline at end of file From 9981741cab6e7c3d2896d63721d380499d49f942 Mon Sep 17 00:00:00 2001 From: Emily Michaud <59289146+emichaud998@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:05:20 -0500 Subject: [PATCH 4/4] Update mock authorization to send back jwt encoded client id for token and address additional PR comments --- .../client/v2.0.0/c4bb_client_test_suite.rb | 7 ++-- .../v2.0.0/endpoints/next_page_endpoint.rb | 4 ++- .../v2.0.0/endpoints/resource_api_endpoint.rb | 4 ++- .../v2.0.0/endpoints/resource_id_endpoint.rb | 4 ++- .../client/v2.0.0/endpoints/token_endpoint.rb | 20 +++-------- .../client/v2.0.0/initial_wait_test.rb | 7 ++-- .../client/v2.0.0/mock_authorization.rb | 35 +++++++++++++++++++ .../client/v2.0.0/mock_server.rb | 6 ++-- .../client/v2.0.0/urls.rb | 27 ++++++++------ .../c4bb_client_initial_wait_test_spec.rb | 14 ++++---- 10 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_authorization.rb diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb index d36e1ed8..44fedf20 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/c4bb_client_test_suite.rb @@ -90,11 +90,11 @@ def self.test_resumes?(test) suite_endpoint :get, BASE_FHIR_PATH, NextPageEndpoint - resume_test_route :get, RESUME_PASS_PATH do |request| - C4BBV200ClientSuite.extract_token_from_query_params(request) + resume_test_route :get, RESUME_CLAIMS_DATA_PATH do |request| + C4BBV200ClientSuite.extract_test_run_identifier_from_query_params(request) end - resume_test_route :get, RESUME_CLAIMS_DATA_PATH do |request| + resume_test_route :get, RESUME_PASS_PATH do |request| C4BBV200ClientSuite.extract_token_from_query_params(request) end @@ -104,6 +104,7 @@ def self.test_resumes?(test) route(:get, METADATA_PATH, get_metadata) route(:get, SMART_CONFIG_PATH, carin_smart_config) + route(:get, JKWS_PATH, MockAuthorization.method(:jwks)) group do run_as_group diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/next_page_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/next_page_endpoint.rb index 85955fbc..977652fa 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/next_page_endpoint.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/next_page_endpoint.rb @@ -1,12 +1,14 @@ require_relative '../tags' require_relative '../mock_server' +require_relative '../mock_authorization' module CarinForBlueButtonTestKit class NextPageEndpoint < Inferno::DSL::SuiteEndpoint include CarinForBlueButtonTestKit::MockServer + include CarinForBlueButtonTestKit::MockAuthorization def test_run_identifier - extract_bearer_token(request) + extract_client_id_from_bearer_token(request) || extract_bearer_token(request) end def make_response diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_api_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_api_endpoint.rb index fb5e8c42..d48ac98f 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_api_endpoint.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_api_endpoint.rb @@ -1,14 +1,16 @@ require_relative '../tags' require_relative '../mock_server' +require_relative '../mock_authorization' require_relative '../client_validation_test' module CarinForBlueButtonTestKit class ResourceAPIEndpoint < Inferno::DSL::SuiteEndpoint include CarinForBlueButtonTestKit::MockServer + include CarinForBlueButtonTestKit::MockAuthorization include CarinForBlueButtonTestKit::ClientValidationTest def test_run_identifier - extract_bearer_token(request) + extract_client_id_from_bearer_token(request) || extract_bearer_token(request) end def make_response diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_id_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_id_endpoint.rb index 4b23a9ef..992556ac 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_id_endpoint.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/resource_id_endpoint.rb @@ -1,12 +1,14 @@ require_relative '../tags' require_relative '../mock_server' +require_relative '../mock_authorization' module CarinForBlueButtonTestKit class ResourceIDEndpoint < Inferno::DSL::SuiteEndpoint include CarinForBlueButtonTestKit::MockServer + include CarinForBlueButtonTestKit::MockAuthorization def test_run_identifier - extract_bearer_token(request) + extract_client_id_from_bearer_token(request) || extract_bearer_token(request) end def make_response diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb index b0c346da..de501f14 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/endpoints/token_endpoint.rb @@ -1,10 +1,12 @@ require_relative '../tags' require_relative '../urls' require_relative '../mock_server' +require_relative '../mock_authorization' module CarinForBlueButtonTestKit module MockAuthorization AUTHORIZED_PRACTITIONER_ID = 'c4bb-Practitioner'.freeze # Must exist on the FHIR_REFERENCE_SERVER (env var) + CARIN_PATIENT_ID = '888'.freeze # Must exist on the FHIR_REFERENCE_SERVER (env var) class TokenEndpoint < Inferno::DSL::SuiteEndpoint include CarinForBlueButtonTestKit::MockServer @@ -19,7 +21,7 @@ def tags def make_response client_id = extract_client_id - access_token = client_id + access_token = JWT.encode({ inferno_client_id: client_id }, nil, 'none') granted_scopes = SUPPORTED_SCOPES & requested_scopes response_hash = { access_token:, scope: granted_scopes.join(' '), token_type: 'bearer', @@ -30,7 +32,7 @@ def make_response fhir_user: granted_scopes.include?('fhirUser'))) end - response_hash.merge!(patient: '888') + response_hash.merge!(patient: CARIN_PATIENT_ID) response.body = response_hash.to_json response.headers['Cache-Control'] = 'no-store' @@ -60,20 +62,6 @@ def extract_client_id_from_client_assertion jwt_payload['iss'] || jwt_payload['sub'] if jwt_payload.present? end - def input_group_prefix - if test.id.include?('static') - 'static' - elsif test.id.include?('adaptive') - 'adaptive' - else - 'resp' - end - end - - def find_test_input(input_name) - JSON.parse(result.input_json)&.find { |input| input['name'] == input_name }&.dig('value') - end - def extract_client_id_from_basic_auth encoded_credentials = request.headers['authorization']&.delete_prefix('Basic ') return unless encoded_credentials.present? diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb index 68ede6de..82c600f7 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/initial_wait_test.rb @@ -11,8 +11,9 @@ class C4BBClientInitialWaitTest < Inferno::Test input :client_id, title: 'Client ID', description: %( - Enter the client ID you will use to connect to this CARIN server via SMART. This client ID will be sent - back as the access token to send requests to this server. + Enter the client ID you will use to connect to this CARIN server via a SMART launch. If you wish to send + requests without performing a SMART launch, enter the Bearer access token you will provide with your + requests here instead. ) config options: { accepts_multiple_requests: true } @@ -73,7 +74,7 @@ class C4BBClientInitialWaitTest < Inferno::Test * ExplanationOfBenefit:payee * ExplanationOfBenefit:* - [Click here](#{resume_claims_data_url}?token=#{client_id}) when done. + [Click here](#{resume_claims_data_url}?test_run_identifier=#{client_id}) when done. ), timeout: 900 ) diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_authorization.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_authorization.rb new file mode 100644 index 00000000..b50843fb --- /dev/null +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_authorization.rb @@ -0,0 +1,35 @@ +module CarinForBlueButtonTestKit + module MockAuthorization + RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.generate(2048) + RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key + + module_function + + def extract_client_id_from_bearer_token(request) + token = request.headers['authorization']&.delete_prefix('Bearer ') + jwt = + begin + JWT.decode(token, nil, false) + rescue StandardError + nil + end + jwt&.first&.dig('inferno_client_id') + end + + def jwks(_env) + response_body = { + keys: [ + { + kty: 'RSA', + alg: 'RS256', + n: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.n.to_s(2), padding: false), + e: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.e.to_s(2), padding: false), + use: 'sig' + } + ] + }.to_json + + [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]] + end + end +end diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb index edd1c060..540607c5 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/mock_server.rb @@ -13,8 +13,6 @@ module MockServer include URLs SUPPORTED_SCOPES = ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access', 'openid', 'fhirUser'].freeze - RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.generate(2048) - RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key def server_proxy @server_proxy ||= Faraday.new( @@ -167,6 +165,10 @@ def extract_token_from_query_params(request) request.query_parameters['token'] end + def extract_test_run_identifier_from_query_params(request) + request.query_parameters['test_run_identifier'] + end + # Pull resource type from url # e.g. http://example.org/fhir/Patient?_id=123 -> Patient # @private diff --git a/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb b/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb index fa4d010f..a54352c5 100644 --- a/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb +++ b/lib/carin_for_blue_button_test_kit/client/v2.0.0/urls.rb @@ -1,15 +1,16 @@ module CarinForBlueButtonTestKit - TOKEN_PATH = '/mock_auth/token' - AUTH_PATH = '/mock_auth/authorization' - SMART_CONFIG_PATH = '/.well-known/smart-configuration' - PATIENT_PATH = '/fhir/Patient' - RESOURCE_API_PATH = '/fhir/:endpoint' - RESOURCE_ID_PATH = '/fhir/:endpoint/:id' - METADATA_PATH = '/fhir/metadata' - BASE_FHIR_PATH = '/fhir' - RESUME_PASS_PATH = '/resume_pass' - RESUME_CLAIMS_DATA_PATH = '/resume_claims_data' - RESUME_FAIL_PATH = '/resume_fail' + TOKEN_PATH = '/mock_auth/token'.freeze + AUTH_PATH = '/mock_auth/authorization'.freeze + JKWS_PATH = '/.well-known/jwks.json'.freeze + SMART_CONFIG_PATH = '/.well-known/smart-configuration'.freeze + PATIENT_PATH = '/fhir/Patient'.freeze + RESOURCE_API_PATH = '/fhir/:endpoint'.freeze + RESOURCE_ID_PATH = '/fhir/:endpoint/:id'.freeze + METADATA_PATH = '/fhir/metadata'.freeze + BASE_FHIR_PATH = '/fhir'.freeze + RESUME_PASS_PATH = '/resume_pass'.freeze + RESUME_CLAIMS_DATA_PATH = '/resume_claims_data'.freeze + RESUME_FAIL_PATH = '/resume_fail'.freeze module URLs def base_url @@ -24,6 +25,10 @@ def authorization_url @authorization_url ||= base_url + AUTH_PATH end + def jwks_url + @jwks_url ||= base_url + JKWS_PATH + end + def smart_configuration_url @smart_configuration_url ||= base_url + SMART_CONFIG_PATH end diff --git a/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb b/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb index 47f3bb16..01dd48f2 100644 --- a/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb +++ b/spec/carin_for_blue_button/c4bb_client/c4bb_client_initial_wait_test_spec.rb @@ -28,8 +28,8 @@ let(:client_id) { 'SAMPLE_TOKEN' } - let(:resume_pass_url) do - "#{Inferno::Application['base_url']}/custom/c4bb_v200_client/resume_claims_data?token=#{client_id}" + let(:resume_claims_data_url) do + "#{Inferno::Application['base_url']}/custom/c4bb_v200_client/resume_claims_data?test_run_identifier=#{client_id}" end let(:c4bb_eob_include_bundle) do @@ -96,7 +96,7 @@ def run(runnable, inputs = {}) expect(response_body['resourceType']).to eq('Bundle') expect(response_body['entry'].first['resource']['resourceType']).to eq('Patient') expect(last_request.env['inferno.tags']).to include('carin_resource_api', 'Patient', '_id') - get(resume_pass_url) + get(resume_claims_data_url) result = results_repo.find(result.id) expect(result.result).to eq('pass') expect(patient_fhir_api_request).to have_been_made @@ -141,7 +141,7 @@ def run(runnable, inputs = {}) '_include=ExplanationOfBenefit:care-team', '_include=ExplanationOfBenefit:coverage', '_include=ExplanationOfBenefit:payee') - get(resume_pass_url) + get(resume_claims_data_url) result = results_repo.find(result.id) expect(result.result).to eq('pass') expect(eob_fhir_include_search).to have_been_made @@ -161,7 +161,7 @@ def run(runnable, inputs = {}) result = results_repo.find(result.id) expect(result.result).to eq('wait') - get(resume_pass_url) + get(resume_claims_data_url) result = results_repo.find(result.id) expect(result.result).to eq('pass') end @@ -189,7 +189,7 @@ def run(runnable, inputs = {}) result = results_repo.find(result.id) expect(result.result).to eq('wait') - get(resume_pass_url) + get(resume_claims_data_url) result = results_repo.find(result.id) expect(result.result).to eq('pass') expect(patient_fhir_api_request).to have_been_made @@ -218,7 +218,7 @@ def run(runnable, inputs = {}) expect(response_body['resourceType']).to eq('Bundle') expect(response_body['entry'].first['resource']['resourceType']).to eq('Patient') - get(resume_pass_url) + get(resume_claims_data_url) result = results_repo.find(result.id) expect(result.result).to eq('pass') expect(patient_fhir_api_request).to have_been_made