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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/inferno/exceptions.rb +31 -0
  4. data/lib/inferno/ext/bloomer.rb +24 -0
  5. data/lib/inferno/repositiories/validators.rb +17 -0
  6. data/lib/inferno/repositiories/value_sets.rb +26 -0
  7. data/lib/inferno/terminology/bcp47.rb +95 -0
  8. data/lib/inferno/terminology/bcp_13.rb +26 -0
  9. data/lib/inferno/terminology/codesystem.rb +49 -0
  10. data/lib/inferno/terminology/expected_manifest.yml +1123 -0
  11. data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
  12. data/lib/inferno/terminology/loader.rb +298 -0
  13. data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
  14. data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
  15. data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
  16. data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
  17. data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
  18. data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
  19. data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
  20. data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
  21. data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
  22. data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
  23. data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
  24. data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
  25. data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
  26. data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
  27. data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
  28. data/lib/inferno/terminology/tasks.rb +11 -0
  29. data/lib/inferno/terminology/terminology_configuration.rb +52 -0
  30. data/lib/inferno/terminology/terminology_validation.rb +42 -0
  31. data/lib/inferno/terminology/validator.rb +64 -0
  32. data/lib/inferno/terminology/value_set.rb +462 -0
  33. data/lib/inferno/terminology.rb +16 -0
  34. data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
  35. data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
  36. data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
  37. data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
  38. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
  39. data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
  40. data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
  41. data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
  42. data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
  43. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
  44. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
  45. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
  46. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
  47. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
  48. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
  49. data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
  50. data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
  51. data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
  52. data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
  53. data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
  54. data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
  55. data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
  56. data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
  57. data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
  58. data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
  59. data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
  60. data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
  61. data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
  62. data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
  63. data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
  64. data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
  65. data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
  66. data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
  67. data/lib/onc_certification_g10_test_kit/version.rb +3 -0
  68. data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
  69. data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
  70. data/lib/onc_certification_g10_test_kit.rb +223 -0
  71. 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