Skip to content
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

FI-3404: Add SMART on FHIR Workflow Support to CARIN Client tests #63

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions config/presets/carin_cpcds_client_ri.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,24 +1,114 @@
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
Copy link

@tstrass tstrass Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token should not be the plain client_id, because we don't want testers to see that, recognize it as the client ID, and think they can expect that from other auth servers. Instead, the client_id should be encoded in the token in some way that is opaque to the client system. In DTR I put it into a JWT, but we intend to change that, because even an encoded JWT is too recognizable. It doesn't really matter how it's encoded as long as it's simple, unrecognizable, and can be decoded.

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

response_hash.merge!(patient: '888')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this referencing a Patient on the reference server? If so I'd move it up to be another constant, so it's consistent with how you're tracking the practitioner ID.


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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is not needed in this test kit


def find_test_input(input_name)
JSON.parse(result.input_json)&.find { |input| input['name'] == input_name }&.dig('value')
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not needed


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')
Copy link

@tstrass tstrass Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're serving a signed ID token, I think you need to also expose the public key in a JWKS, e.g. https://github.com/inferno-framework/davinci-dtr-test-kit/blob/main/lib/davinci_dtr_test_kit/endpoints/mock_authorization.rb#L20

end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest also updating the query parameter name since it's no longer a token. Could be client_id, or even just test_run_identifier.

),
timeout: 900
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Loading
Loading