onc_certification_g10_test_kit 2.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/inferno/exceptions.rb +31 -0
- data/lib/inferno/ext/bloomer.rb +24 -0
- data/lib/inferno/repositiories/validators.rb +17 -0
- data/lib/inferno/repositiories/value_sets.rb +26 -0
- data/lib/inferno/terminology/bcp47.rb +95 -0
- data/lib/inferno/terminology/bcp_13.rb +26 -0
- data/lib/inferno/terminology/codesystem.rb +49 -0
- data/lib/inferno/terminology/expected_manifest.yml +1123 -0
- data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
- data/lib/inferno/terminology/loader.rb +298 -0
- data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
- data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
- data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
- data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
- data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
- data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
- data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
- data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
- data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
- data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
- data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
- data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
- data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
- data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
- data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
- data/lib/inferno/terminology/tasks.rb +11 -0
- data/lib/inferno/terminology/terminology_configuration.rb +52 -0
- data/lib/inferno/terminology/terminology_validation.rb +42 -0
- data/lib/inferno/terminology/validator.rb +64 -0
- data/lib/inferno/terminology/value_set.rb +462 -0
- data/lib/inferno/terminology.rb +16 -0
- data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
- data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
- data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
- data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
- data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
- data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
- data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
- data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
- data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
- data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
- data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
- data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
- data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
- data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
- data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
- data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
- data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
- data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
- data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
- data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
- data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
- data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
- data/lib/onc_certification_g10_test_kit/version.rb +3 -0
- data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
- data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
- data/lib/onc_certification_g10_test_kit.rb +223 -0
- metadata +310 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module ONCCertificationG10TestKit
|
|
2
|
+
class SMARTAppLaunchInvalidAudGroup < Inferno::TestGroup
|
|
3
|
+
title 'SMART App Launch Error: Invalid AUD Parameter'
|
|
4
|
+
short_title 'SMART Invalid AUD Launch'
|
|
5
|
+
description %(
|
|
6
|
+
# Background
|
|
7
|
+
|
|
8
|
+
The Invalid AUD Sequence verifies that a SMART Launch Sequence,
|
|
9
|
+
specifically the [Standalone
|
|
10
|
+
Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
11
|
+
Sequence, does not work in the case where the client sends an invalid FHIR
|
|
12
|
+
server as the `aud` parameter during launch. This must fail to ensure that
|
|
13
|
+
a genuine bearer token is not leaked to a counterfit resource server.
|
|
14
|
+
|
|
15
|
+
This test is not included as part of a regular SMART Launch Sequence
|
|
16
|
+
because it requires the browser of the user to be redirected to the
|
|
17
|
+
authorization service, and there is no expectation that the authorization
|
|
18
|
+
service redirects the user back to Inferno with an error message. The only
|
|
19
|
+
requirement is that Inferno is not granted a code to exchange for a valid
|
|
20
|
+
access token. Since this is a special case, it is tested independently in
|
|
21
|
+
a separate sequence.
|
|
22
|
+
|
|
23
|
+
Note that this test will launch a new browser window. The user is required
|
|
24
|
+
to 'Attest' in the Inferno user interface after the launch does not
|
|
25
|
+
succeed, if the server does not return an error code.
|
|
26
|
+
)
|
|
27
|
+
id :g10_smart_invalid_aud
|
|
28
|
+
run_as_group
|
|
29
|
+
|
|
30
|
+
input :client_id,
|
|
31
|
+
:client_secret,
|
|
32
|
+
:requested_scopes,
|
|
33
|
+
:url,
|
|
34
|
+
:smart_authorization_url,
|
|
35
|
+
:smart_token_url
|
|
36
|
+
|
|
37
|
+
config(
|
|
38
|
+
inputs: {
|
|
39
|
+
client_id: {
|
|
40
|
+
name: :standalone_client_id,
|
|
41
|
+
title: 'Standalone Client ID',
|
|
42
|
+
description: 'Client ID provided during registration of Inferno as a standalone application'
|
|
43
|
+
},
|
|
44
|
+
client_secret: {
|
|
45
|
+
name: :standalone_client_secret,
|
|
46
|
+
title: 'Standalone Client Secret',
|
|
47
|
+
description: 'Client Secret provided during registration of Inferno as a standalone application'
|
|
48
|
+
},
|
|
49
|
+
requested_scopes: {
|
|
50
|
+
name: :standalone_requested_scopes,
|
|
51
|
+
title: 'Standalone Scope',
|
|
52
|
+
description: 'OAuth 2.0 scope provided by system to enable all required functionality',
|
|
53
|
+
type: 'textarea',
|
|
54
|
+
default: %(
|
|
55
|
+
launch/patient openid fhirUser offline_access
|
|
56
|
+
patient/Medication.read patient/AllergyIntolerance.read
|
|
57
|
+
patient/CarePlan.read patient/CareTeam.read patient/Condition.read
|
|
58
|
+
patient/Device.read patient/DiagnosticReport.read
|
|
59
|
+
patient/DocumentReference.read patient/Encounter.read
|
|
60
|
+
patient/Goal.read patient/Immunization.read patient/Location.read
|
|
61
|
+
patient/MedicationRequest.read patient/Observation.read
|
|
62
|
+
patient/Organization.read patient/Patient.read
|
|
63
|
+
patient/Practitioner.read patient/Procedure.read
|
|
64
|
+
patient/Provenance.read patient/PractitionerRole.read
|
|
65
|
+
).gsub(/\s{2,}/, ' ').strip
|
|
66
|
+
},
|
|
67
|
+
url: {
|
|
68
|
+
title: 'Standalone FHIR Endpoint',
|
|
69
|
+
description: 'URL of the FHIR endpoint used by standalone applications'
|
|
70
|
+
},
|
|
71
|
+
smart_authorization_url: {
|
|
72
|
+
title: 'OAuth 2.0 Authorize Endpoint',
|
|
73
|
+
description: 'OAuth 2.0 Authorize Endpoint provided during the patient standalone launch'
|
|
74
|
+
},
|
|
75
|
+
smart_token_url: {
|
|
76
|
+
title: 'OAuth 2.0 Token Endpoint',
|
|
77
|
+
description: 'OAuth 2.0 Token Endpoint provided during the patient standalone launch'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
outputs: {
|
|
81
|
+
state: { name: :invalid_aud_state }
|
|
82
|
+
},
|
|
83
|
+
requests: {
|
|
84
|
+
redirect: { name: :invalid_aud_redirect }
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
test from: :smart_app_redirect do
|
|
89
|
+
def aud
|
|
90
|
+
'https://inferno.healthit.gov/invalid_aud'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def wait_message(auth_url)
|
|
94
|
+
%(
|
|
95
|
+
Inferno will redirect you to an external website for authorization.
|
|
96
|
+
**It is expected this will fail**. If the server does not return to
|
|
97
|
+
Inferno automatically, but does provide an error message, you may
|
|
98
|
+
return to Inferno and confirm that an error was presented in this
|
|
99
|
+
window.
|
|
100
|
+
|
|
101
|
+
* [Perform Invalid Launch](#{auth_url})
|
|
102
|
+
* [Attest launch failed](/custom/smart/redirect?state=#{state}&confirm_fail=true)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
test do
|
|
108
|
+
title 'Inferno client app does not receive code parameter redirect URI'
|
|
109
|
+
description %(
|
|
110
|
+
Inferno redirected the user to the authorization service with an invalid AUD.
|
|
111
|
+
Inferno expects that the authorization request will not succeed. This can
|
|
112
|
+
either be from the server explicitely pass an error, or stopping and the
|
|
113
|
+
tester returns to Inferno to confirm that the server presented them a failure.
|
|
114
|
+
)
|
|
115
|
+
uses_request :redirect
|
|
116
|
+
|
|
117
|
+
run do
|
|
118
|
+
params = request.query_parameters
|
|
119
|
+
|
|
120
|
+
assert params['code'].blank?,
|
|
121
|
+
'Authorization has incorrectly succeeded because access code provided to Inferno.'
|
|
122
|
+
|
|
123
|
+
pass_message =
|
|
124
|
+
if params['error'].present?
|
|
125
|
+
'Server redirected the user back to the app with an error.'
|
|
126
|
+
elsif params['confirm_fail']
|
|
127
|
+
'Tester attested that the authorization service did not succeed due to invalid AUD parameter.'
|
|
128
|
+
else
|
|
129
|
+
'Server redirected the user back to the app without an access code.'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
pass pass_message
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
require_relative 'base_token_refresh_group'
|
|
2
|
+
require_relative 'smart_scopes_test'
|
|
3
|
+
require_relative 'unauthorized_access_test'
|
|
4
|
+
require_relative 'well_known_capabilities_test'
|
|
5
|
+
|
|
6
|
+
module ONCCertificationG10TestKit
|
|
7
|
+
class SmartEHRPractitionerAppGroup < Inferno::TestGroup
|
|
8
|
+
title 'EHR Practitioner App'
|
|
9
|
+
short_title 'EHR Practitioner App'
|
|
10
|
+
input_instructions %(
|
|
11
|
+
Register Inferno as an EHR-launched application using the following information:
|
|
12
|
+
|
|
13
|
+
* Launch URI: `#{SMARTAppLaunch::AppLaunchTest.config.options[:launch_uri]}`
|
|
14
|
+
* Redirect URI: `#{SMARTAppLaunch::AppRedirectTest.config.options[:redirect_uri]}`
|
|
15
|
+
|
|
16
|
+
Enter in the appropriate scope to enable user-level access to all relevant
|
|
17
|
+
resources. In addition, support for the OpenID Connect (openid fhirUser),
|
|
18
|
+
refresh tokens (offline_access), and EHR context (launch) are required. This
|
|
19
|
+
test expects that the EHR will launch the application with a patient context.
|
|
20
|
+
|
|
21
|
+
After submit is pressed, Inferno will wait for the system under test to launch
|
|
22
|
+
the application.
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
description %(
|
|
26
|
+
Demonstrate the ability to perform an EHR launch to a [SMART on
|
|
27
|
+
FHIR](http://www.hl7.org/fhir/smart-app-launch/) confidential client with
|
|
28
|
+
patient context, refresh token, and [OpenID Connect
|
|
29
|
+
(OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) identity
|
|
30
|
+
token. After launch, a simple Patient resource read is performed on the
|
|
31
|
+
patient in context. The access token is then refreshed, and the Patient
|
|
32
|
+
resource is read using the new access token to ensure that the refresh was
|
|
33
|
+
successful. Finally, the authentication information provided by OpenID
|
|
34
|
+
Connect is decoded and validated.
|
|
35
|
+
)
|
|
36
|
+
id :g10_smart_ehr_practitioner_app
|
|
37
|
+
run_as_group
|
|
38
|
+
|
|
39
|
+
config(
|
|
40
|
+
inputs: {
|
|
41
|
+
client_secret: {
|
|
42
|
+
optional: false
|
|
43
|
+
},
|
|
44
|
+
smart_credentials: {
|
|
45
|
+
name: :ehr_smart_credentials
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
group from: :smart_discovery do
|
|
51
|
+
test from: 'g10_smart_well_known_capabilities'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
group from: :smart_ehr_launch do
|
|
55
|
+
title 'EHR Launch With Practitioner Scope'
|
|
56
|
+
|
|
57
|
+
config(
|
|
58
|
+
inputs: {
|
|
59
|
+
requested_scopes: {
|
|
60
|
+
default: %(
|
|
61
|
+
launch openid fhirUser offline_access user/Medication.read
|
|
62
|
+
user/AllergyIntolerance.read user/CarePlan.read user/CareTeam.read
|
|
63
|
+
user/Condition.read user/Device.read user/DiagnosticReport.read
|
|
64
|
+
user/DocumentReference.read user/Encounter.read user/Goal.read
|
|
65
|
+
user/Immunization.read user/Location.read
|
|
66
|
+
user/MedicationRequest.read user/Observation.read
|
|
67
|
+
user/Organization.read user/Patient.read user/Practitioner.read
|
|
68
|
+
user/Procedure.read user/Provenance.read
|
|
69
|
+
user/PractitionerRole.read
|
|
70
|
+
).gsub(/\s{2,}/, ' ').strip
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
test from: :g10_smart_scopes do
|
|
76
|
+
title 'User-level access with OpenID Connect and Refresh Token scopes used.'
|
|
77
|
+
config(
|
|
78
|
+
inputs: {
|
|
79
|
+
requested_scopes: { name: :ehr_requested_scopes },
|
|
80
|
+
received_scopes: { name: :ehr_received_scopes }
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def required_scopes
|
|
85
|
+
['openid', 'fhirUser', 'launch', 'offline_access']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def scope_type
|
|
89
|
+
'user'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
test from: :g10_unauthorized_access,
|
|
94
|
+
config: {
|
|
95
|
+
inputs: {
|
|
96
|
+
patient_id: { name: :ehr_patient_id }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test from: :g10_patient_context,
|
|
101
|
+
config: {
|
|
102
|
+
inputs: {
|
|
103
|
+
patient_id: { name: :ehr_patient_id },
|
|
104
|
+
access_token: { name: :ehr_access_token }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
test do
|
|
109
|
+
title 'Launch context contains smart_style_url which links to valid JSON'
|
|
110
|
+
description %(
|
|
111
|
+
In order to mimic the style of the SMART host more closely, SMART apps
|
|
112
|
+
can check for the existence of this launch context parameter and
|
|
113
|
+
download the JSON file referenced by the URL value.
|
|
114
|
+
)
|
|
115
|
+
uses_request :token
|
|
116
|
+
|
|
117
|
+
run do
|
|
118
|
+
skip_if request.status != 200, 'No token response received'
|
|
119
|
+
assert_valid_json response[:body]
|
|
120
|
+
|
|
121
|
+
body = JSON.parse(response[:body])
|
|
122
|
+
|
|
123
|
+
assert body['smart_style_url'].present?,
|
|
124
|
+
'Token response did not contain `smart_style_url`'
|
|
125
|
+
|
|
126
|
+
get(body['smart_style_url'])
|
|
127
|
+
|
|
128
|
+
assert_response_status(200)
|
|
129
|
+
assert_valid_json(response[:body])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
test do
|
|
134
|
+
title 'Launch context contains need_patient_banner'
|
|
135
|
+
description %(
|
|
136
|
+
`need_patient_banner` is a boolean value indicating whether the app
|
|
137
|
+
was launched in a UX context where a patient banner is required (when
|
|
138
|
+
true) or not required (when false).
|
|
139
|
+
)
|
|
140
|
+
uses_request :token
|
|
141
|
+
|
|
142
|
+
run do
|
|
143
|
+
skip_if request.status != 200, 'No token response received'
|
|
144
|
+
assert_valid_json response[:body]
|
|
145
|
+
|
|
146
|
+
body = JSON.parse(response[:body])
|
|
147
|
+
|
|
148
|
+
assert body.key?('need_patient_banner'),
|
|
149
|
+
'Token response did not contain `need_patient_banner`'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
group from: :smart_openid_connect,
|
|
155
|
+
config: {
|
|
156
|
+
inputs: {
|
|
157
|
+
id_token: { name: :ehr_id_token },
|
|
158
|
+
client_id: { name: :ehr_client_id },
|
|
159
|
+
requested_scopes: { name: :ehr_requested_scopes },
|
|
160
|
+
smart_credentials: { name: :ehr_smart_credentials }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
group from: :g10_token_refresh do
|
|
165
|
+
id :g10_smart_ehr_token_refresh
|
|
166
|
+
|
|
167
|
+
config(
|
|
168
|
+
inputs: {
|
|
169
|
+
refresh_token: { name: :ehr_refresh_token },
|
|
170
|
+
client_id: { name: :ehr_client_id },
|
|
171
|
+
client_secret: { name: :ehr_client_secret },
|
|
172
|
+
received_scopes: { name: :ehr_received_scopes }
|
|
173
|
+
},
|
|
174
|
+
outputs: {
|
|
175
|
+
refresh_token: { name: :ehr_refresh_token },
|
|
176
|
+
received_scopes: { name: :ehr_received_scopes },
|
|
177
|
+
access_token: { name: :ehr_access_token },
|
|
178
|
+
token_retrieval_time: { name: :ehr_token_retrieval_time },
|
|
179
|
+
expires_in: { name: :ehr_expires_in },
|
|
180
|
+
smart_credentials: { name: :ehr_smart_credentials }
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
test from: :g10_patient_context do
|
|
185
|
+
config(
|
|
186
|
+
inputs: {
|
|
187
|
+
patient_id: { name: :ehr_patient_id }
|
|
188
|
+
},
|
|
189
|
+
options: {
|
|
190
|
+
refresh_test: true
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
uses_request :token_refresh
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
test do
|
|
198
|
+
id :g10_ehr_credentials_export
|
|
199
|
+
title 'Set SMART Credentials to EHR Launch Credentials'
|
|
200
|
+
|
|
201
|
+
input :ehr_smart_credentials, type: :oauth_credentials
|
|
202
|
+
output :smart_credentials
|
|
203
|
+
|
|
204
|
+
run do
|
|
205
|
+
output smart_credentials: ehr_smart_credentials.to_s
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
module ONCCertificationG10TestKit
|
|
2
|
+
class SMARTInvalidTokenGroup < Inferno::TestGroup
|
|
3
|
+
title 'SMART App Launch Error: Invalid Access Token Request'
|
|
4
|
+
short_title 'SMART Invalid Token Request'
|
|
5
|
+
description %(
|
|
6
|
+
# Background
|
|
7
|
+
|
|
8
|
+
The Invalid Access Token Request Sequence verifies that a SMART Launch
|
|
9
|
+
Sequence, specifically the [Standalone
|
|
10
|
+
Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
11
|
+
Sequence, does not work in the case where the client sends an invalid
|
|
12
|
+
Authorization code or client ID during the code exchange step. This must
|
|
13
|
+
not result in a successful launch.
|
|
14
|
+
|
|
15
|
+
This test is not included as part of a regular SMART Launch Sequence
|
|
16
|
+
because some servers may not accept an authorization code after it has
|
|
17
|
+
been used unsuccessfully in this manner.
|
|
18
|
+
)
|
|
19
|
+
id :g10_smart_invalid_token_request
|
|
20
|
+
run_as_group
|
|
21
|
+
|
|
22
|
+
input :client_id, :client_secret, :requested_scopes, :url, :smart_authorization_url, :smart_token_url
|
|
23
|
+
|
|
24
|
+
input :use_pkce,
|
|
25
|
+
title: 'Proof Key for Code Exchange (PKCE)',
|
|
26
|
+
type: 'radio',
|
|
27
|
+
default: 'false',
|
|
28
|
+
options: {
|
|
29
|
+
list_options: [
|
|
30
|
+
{
|
|
31
|
+
label: 'Enabled',
|
|
32
|
+
value: 'true'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'Disabled',
|
|
36
|
+
value: 'false'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
input :pkce_code_challenge_method,
|
|
41
|
+
optional: true,
|
|
42
|
+
title: 'PKCE Code Challenge Method',
|
|
43
|
+
type: 'radio',
|
|
44
|
+
default: 'S256',
|
|
45
|
+
options: {
|
|
46
|
+
list_options: [
|
|
47
|
+
{
|
|
48
|
+
label: 'S256',
|
|
49
|
+
value: 'S256'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'plain',
|
|
53
|
+
value: 'plain'
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
config(
|
|
59
|
+
inputs: {
|
|
60
|
+
client_id: {
|
|
61
|
+
name: :standalone_client_id,
|
|
62
|
+
title: 'Standalone Client ID',
|
|
63
|
+
description: 'Client ID provided during registration of Inferno as a standalone application'
|
|
64
|
+
},
|
|
65
|
+
client_secret: {
|
|
66
|
+
name: :standalone_client_secret,
|
|
67
|
+
title: 'Standalone Client Secret',
|
|
68
|
+
description: 'Client Secret provided during registration of Inferno as a standalone application'
|
|
69
|
+
},
|
|
70
|
+
requested_scopes: {
|
|
71
|
+
name: :standalone_requested_scopes,
|
|
72
|
+
title: 'Standalone Scope',
|
|
73
|
+
description: 'OAuth 2.0 scope provided by system to enable all required functionality',
|
|
74
|
+
type: 'textarea',
|
|
75
|
+
default: %(
|
|
76
|
+
launch/patient openid fhirUser offline_access
|
|
77
|
+
patient/Medication.read patient/AllergyIntolerance.read
|
|
78
|
+
patient/CarePlan.read patient/CareTeam.read patient/Condition.read
|
|
79
|
+
patient/Device.read patient/DiagnosticReport.read
|
|
80
|
+
patient/DocumentReference.read patient/Encounter.read
|
|
81
|
+
patient/Goal.read patient/Immunization.read patient/Location.read
|
|
82
|
+
patient/MedicationRequest.read patient/Observation.read
|
|
83
|
+
patient/Organization.read patient/Patient.read
|
|
84
|
+
patient/Practitioner.read patient/Procedure.read
|
|
85
|
+
patient/Provenance.read patient/PractitionerRole.read
|
|
86
|
+
).gsub(/\s{2,}/, ' ').strip
|
|
87
|
+
},
|
|
88
|
+
url: {
|
|
89
|
+
title: 'Standalone FHIR Endpoint',
|
|
90
|
+
description: 'URL of the FHIR endpoint used by standalone applications'
|
|
91
|
+
},
|
|
92
|
+
code: {
|
|
93
|
+
name: :invalid_token_code
|
|
94
|
+
},
|
|
95
|
+
state: {
|
|
96
|
+
name: :invalid_token_state
|
|
97
|
+
},
|
|
98
|
+
smart_authorization_url: {
|
|
99
|
+
title: 'OAuth 2.0 Authorize Endpoint',
|
|
100
|
+
description: 'OAuth 2.0 Authorize Endpoint provided during the patient standalone launch'
|
|
101
|
+
},
|
|
102
|
+
smart_token_url: {
|
|
103
|
+
title: 'OAuth 2.0 Token Endpoint',
|
|
104
|
+
description: 'OAuth 2.0 Token Endpoint provided during the patient standalone launch'
|
|
105
|
+
},
|
|
106
|
+
pkce_code_verifier: {
|
|
107
|
+
name: :invalid_token_pkce_code_verifier
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
outputs: {
|
|
111
|
+
code: { name: :invalid_token_code },
|
|
112
|
+
state: { name: :invalid_token_state },
|
|
113
|
+
expires_in: { name: :invalid_token_expires_in },
|
|
114
|
+
pkce_code_verifier: { name: :invalid_token_pkce_code_verifier }
|
|
115
|
+
},
|
|
116
|
+
requests: {
|
|
117
|
+
redirect: { name: :invalid_token_redirect },
|
|
118
|
+
token: { name: :invalid_token_token }
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
test from: :smart_app_redirect
|
|
123
|
+
test from: :smart_code_received
|
|
124
|
+
|
|
125
|
+
test do
|
|
126
|
+
title ' OAuth token exchange fails when supplied invalid code'
|
|
127
|
+
description %(
|
|
128
|
+
If the request failed verification or is invalid, the authorization
|
|
129
|
+
server returns an error response.
|
|
130
|
+
)
|
|
131
|
+
uses_request :redirect
|
|
132
|
+
|
|
133
|
+
input :use_pkce, :pkce_code_verifier
|
|
134
|
+
|
|
135
|
+
run do
|
|
136
|
+
skip_if request.query_parameters['error'].present?, 'Error during authorization request'
|
|
137
|
+
|
|
138
|
+
oauth2_params = {
|
|
139
|
+
grant_type: 'authorization_code',
|
|
140
|
+
code: 'BAD_CODE',
|
|
141
|
+
redirect_uri: config.options[:redirect_uri]
|
|
142
|
+
}
|
|
143
|
+
oauth2_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
|
144
|
+
|
|
145
|
+
if client_secret.present?
|
|
146
|
+
client_credentials = "#{client_id}:#{client_secret}"
|
|
147
|
+
oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
|
|
148
|
+
else
|
|
149
|
+
oauth2_params[:client_id] = client_id
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
oauth2_params[:code_verifier] = pkce_code_verifier if use_pkce == 'true'
|
|
153
|
+
|
|
154
|
+
post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
|
|
155
|
+
|
|
156
|
+
assert_response_status(400)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
test do
|
|
161
|
+
title 'OAuth token exchange fails when supplied invalid client ID'
|
|
162
|
+
description %(
|
|
163
|
+
If the request failed verification or is invalid, the authorization
|
|
164
|
+
server returns an error response.
|
|
165
|
+
)
|
|
166
|
+
uses_request :redirect
|
|
167
|
+
|
|
168
|
+
input :use_pkce, :pkce_code_verifier, :code
|
|
169
|
+
|
|
170
|
+
run do
|
|
171
|
+
skip_if request.query_parameters['error'].present?, 'Error during authorization request'
|
|
172
|
+
|
|
173
|
+
client_id = 'BAD_CLIENT_ID'
|
|
174
|
+
|
|
175
|
+
oauth2_params = {
|
|
176
|
+
grant_type: 'authorization_code',
|
|
177
|
+
code: code,
|
|
178
|
+
redirect_uri: config.options[:redirect_uri]
|
|
179
|
+
}
|
|
180
|
+
oauth2_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
|
181
|
+
|
|
182
|
+
if client_secret.present?
|
|
183
|
+
client_credentials = "#{client_id}:#{client_secret}"
|
|
184
|
+
oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
|
|
185
|
+
else
|
|
186
|
+
oauth2_params[:client_id] = client_id
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
oauth2_params[:code_verifier] = pkce_code_verifier if use_pkce == 'true'
|
|
190
|
+
|
|
191
|
+
post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
|
|
192
|
+
|
|
193
|
+
assert_response_status([400, 401])
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require_relative 'patient_context_test'
|
|
2
|
+
require_relative 'limited_scope_grant_test'
|
|
3
|
+
require_relative 'restricted_resource_type_access_group'
|
|
4
|
+
|
|
5
|
+
module ONCCertificationG10TestKit
|
|
6
|
+
class SmartLimitedAppGroup < Inferno::TestGroup
|
|
7
|
+
title 'Standalone Patient App - Limited Access'
|
|
8
|
+
short_title 'Limited Access App'
|
|
9
|
+
|
|
10
|
+
input_instructions %(
|
|
11
|
+
The purpose of this test is to demonstrate that users can restrict access
|
|
12
|
+
granted to apps to a limited number of resources. Enter which resources the
|
|
13
|
+
user will grant access to below, and during the launch process only grant
|
|
14
|
+
access to those resources. Inferno will verify that access granted matches
|
|
15
|
+
these expectations.
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
description %(
|
|
19
|
+
This scenario demonstrates the ability to perform a Patient Standalone
|
|
20
|
+
Launch to a [SMART on FHIR](http://www.hl7.org/fhir/smart-app-launch/)
|
|
21
|
+
confidential client with limited access granted to the app based on user
|
|
22
|
+
input. The tester is expected to grant the application access to a subset
|
|
23
|
+
of desired resource types.
|
|
24
|
+
)
|
|
25
|
+
id :g10_smart_limited_app
|
|
26
|
+
run_as_group
|
|
27
|
+
|
|
28
|
+
group from: :smart_standalone_launch do
|
|
29
|
+
title 'Standalone Launch With Limited Scope'
|
|
30
|
+
description %(
|
|
31
|
+
# Background
|
|
32
|
+
|
|
33
|
+
The [Standalone
|
|
34
|
+
Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
35
|
+
Sequence allows an app, like Inferno, to be launched independent of an
|
|
36
|
+
existing EHR session. It is one of the two launch methods described in
|
|
37
|
+
the SMART App Launch Framework alongside EHR Launch. The app will
|
|
38
|
+
request authorization for the provided scope from the authorization
|
|
39
|
+
endpoint, ultimately receiving an authorization token which can be used
|
|
40
|
+
to gain access to resources on the FHIR server.
|
|
41
|
+
|
|
42
|
+
# Test Methodology
|
|
43
|
+
|
|
44
|
+
Inferno will redirect the user to the the authorization endpoint so that
|
|
45
|
+
they may provide any required credentials and authorize the application.
|
|
46
|
+
Upon successful authorization, Inferno will exchange the authorization
|
|
47
|
+
code provided for an access token.
|
|
48
|
+
|
|
49
|
+
For more information on the #{title}:
|
|
50
|
+
|
|
51
|
+
* [Standalone Launch
|
|
52
|
+
Sequence](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
config(
|
|
56
|
+
inputs: {
|
|
57
|
+
client_id: { locked: true },
|
|
58
|
+
client_secret: { locked: true },
|
|
59
|
+
url: { locked: true },
|
|
60
|
+
code: { name: :limited_code },
|
|
61
|
+
state: { name: :limited_state },
|
|
62
|
+
patient_id: { name: :limited_patient_id },
|
|
63
|
+
access_token: { name: :limited_access_token },
|
|
64
|
+
requested_scopes: { name: :limited_requested_scopes },
|
|
65
|
+
smart_authorization_url: { locked: true }, # TODO: separate standalone/ehr discovery outputs
|
|
66
|
+
smart_token_url: { locked: true }, # TODO: separate standalone/ehr discovery outputs
|
|
67
|
+
received_scopes: { name: :limited_received_scopes },
|
|
68
|
+
smart_credentials: { name: :limited_smart_credentials }
|
|
69
|
+
},
|
|
70
|
+
outputs: {
|
|
71
|
+
code: { name: :limited_code },
|
|
72
|
+
token_retrieval_time: { name: :limited_token_retrieval_time },
|
|
73
|
+
state: { name: :limited_state },
|
|
74
|
+
id_token: { name: :limited_id_token },
|
|
75
|
+
refresh_token: { name: :limited_refresh_token },
|
|
76
|
+
access_token: { name: :limited_access_token },
|
|
77
|
+
expires_in: { name: :limited_expires_in },
|
|
78
|
+
patient_id: { name: :limited_patient_id },
|
|
79
|
+
encounter_id: { name: :limited_encounter_id },
|
|
80
|
+
received_scopes: { name: :limited_received_scopes },
|
|
81
|
+
intent: { name: :limited_intent },
|
|
82
|
+
smart_credentials: { name: :limited_smart_credentials }
|
|
83
|
+
},
|
|
84
|
+
requests: {
|
|
85
|
+
redirect: { name: :limited_redirect },
|
|
86
|
+
token: { name: :limited_token }
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
input :expected_resources,
|
|
91
|
+
title: 'Expected Resource Grant',
|
|
92
|
+
description: 'The user will only grant access to the following resources during authorization.',
|
|
93
|
+
default: 'Patient, Condition, Observation'
|
|
94
|
+
|
|
95
|
+
test from: :g10_patient_context,
|
|
96
|
+
config: {
|
|
97
|
+
inputs: {
|
|
98
|
+
patient_id: { name: :limited_patient_id },
|
|
99
|
+
smart_credentials: { name: :limited_smart_credentials }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test from: :g10_limited_scope_grant do
|
|
104
|
+
config(
|
|
105
|
+
inputs: {
|
|
106
|
+
requested_scopes: { name: :limited_requested_scopes },
|
|
107
|
+
received_scopes: { name: :limited_received_scopes }
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
group from: :g10_restricted_resource_type_access,
|
|
114
|
+
config: {
|
|
115
|
+
inputs: {
|
|
116
|
+
patient_id: { name: :limited_patient_id },
|
|
117
|
+
requested_scopes: { name: :limited_requested_scopes },
|
|
118
|
+
received_scopes: { name: :limited_received_scopes },
|
|
119
|
+
smart_credentials: { name: :limited_smart_credentials }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|