onc_certification_g10_test_kit 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|