Skip to content

Commit

Permalink
Add SMART authorize and token endpoints so that clients can connect v…
Browse files Browse the repository at this point in the history
…ia SMART. Update wait test to use client id so that it can be used both for the SMART requests and resource api requests
  • Loading branch information
emichaud998 committed Dec 18, 2024
1 parent 37bddd8 commit d4a5eb4
Show file tree
Hide file tree
Showing 27 changed files with 273 additions and 76 deletions.
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,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
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.
),
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

0 comments on commit d4a5eb4

Please sign in to comment.