-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
d4a5eb4
ae3fcec
e250aae
9981741
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
) | ||
|
There was a problem hiding this comment.
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.