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.
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