udap_security_test_kit 0.11.1 → 0.11.3

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 (24) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/UDAP_RunClientAgainstServer.json.erb +20 -0
  3. data/config/presets/UDAP_RunServerAgainstClient.json.erb +272 -0
  4. data/lib/udap_security_test_kit/client_credentials_token_exchange_test.rb +1 -1
  5. data/lib/udap_security_test_kit/client_suite/client_access_group.rb +22 -0
  6. data/lib/udap_security_test_kit/client_suite/client_access_interaction_test.rb +53 -0
  7. data/lib/udap_security_test_kit/client_suite/client_registration_group.rb +26 -0
  8. data/lib/udap_security_test_kit/client_suite/client_registration_interaction_test.rb +50 -0
  9. data/lib/udap_security_test_kit/client_suite/client_registration_verification_test.rb +244 -0
  10. data/lib/udap_security_test_kit/client_suite/client_token_request_verification_test.rb +178 -0
  11. data/lib/udap_security_test_kit/client_suite/client_token_use_verification_test.rb +43 -0
  12. data/lib/udap_security_test_kit/client_suite.rb +78 -0
  13. data/lib/udap_security_test_kit/docs/demo/FHIR Request.postman_collection.json +81 -0
  14. data/lib/udap_security_test_kit/docs/udap_client_suite_description.md +120 -0
  15. data/lib/udap_security_test_kit/endpoints/echoing_fhir_responder.rb +52 -0
  16. data/lib/udap_security_test_kit/endpoints/mock_udap_server/registration.rb +57 -0
  17. data/lib/udap_security_test_kit/endpoints/mock_udap_server/token.rb +27 -0
  18. data/lib/udap_security_test_kit/endpoints/mock_udap_server.rb +301 -0
  19. data/lib/udap_security_test_kit/metadata.rb +5 -5
  20. data/lib/udap_security_test_kit/tags.rb +8 -0
  21. data/lib/udap_security_test_kit/urls.rb +45 -0
  22. data/lib/udap_security_test_kit/version.rb +2 -1
  23. data/lib/udap_security_test_kit.rb +8 -2
  24. metadata +20 -2
@@ -0,0 +1,244 @@
1
+ require_relative '../tags'
2
+ require_relative '../urls'
3
+ require_relative '../endpoints/mock_udap_server'
4
+
5
+ module UDAPSecurityTestKit
6
+ class UDAPClientRegistrationVerification < Inferno::Test
7
+ include URLs
8
+
9
+ id :udap_client_registration_verification
10
+ title 'Verify UDAP Registration'
11
+ description %(
12
+ During this test, Inferno will verify that the client's UDAP
13
+ registration request is conformant.
14
+ )
15
+ input :udap_client_uri,
16
+ optional: false
17
+
18
+ run do
19
+ omit_if udap_client_uri.blank?, # for re-use: mark the udap_client_uri input as optional when importing to enable
20
+ 'Not configured for UDAP authentication.'
21
+
22
+ load_tagged_requests(UDAP_TAG, REGISTRATION_TAG)
23
+ skip_if requests.empty?, 'No UDAP Registration Requests made.'
24
+
25
+ verified_request = requests.last
26
+ parsed_body = MockUDAPServer.parsed_request_body(verified_request)
27
+ assert parsed_body.present?, 'Registration request body is not valid JSON.'
28
+
29
+ check_request_body(parsed_body)
30
+ check_software_statement(parsed_body['software_statement'], verified_request.created_at)
31
+
32
+ assert messages.none? { |msg|
33
+ msg[:type] == 'error'
34
+ }, 'Invalid registration request. See messages for details.'
35
+ end
36
+
37
+ def check_request_body(request_body)
38
+ if request_body['udap'].blank?
39
+ add_message('error', '`udap` key with a value of `1` missing in the registration request')
40
+ elsif request_body['udap'] != '1'
41
+ add_message('error',
42
+ 'The registration request contained an incorrect `udap` value: expected `1`, ' \
43
+ "got `#{request_body['udap']}`")
44
+ end
45
+
46
+ return unless request_body['certifications'].present?
47
+
48
+ request_body['certifications'].each_with_index do |certification_jwt, index|
49
+ JWT.decond(certification_jwt)
50
+ rescue StandardError => e
51
+ add_message('error',
52
+ "Certification #{index + 1} in the registration request is not a valid signed jwt: #{e}")
53
+ end
54
+ end
55
+
56
+ def check_software_statement(software_statement_jwt, request_time)
57
+ unless software_statement_jwt.present?
58
+ add_message('error',
59
+ 'Registration is missing a `software_statement` key')
60
+ return
61
+ end
62
+
63
+ claims, _headers = begin
64
+ JWT.decode(software_statement_jwt, nil, false)
65
+ rescue StandardError => e
66
+ add_message('error',
67
+ "Registration software statement does not follow the jwt structure: #{e}")
68
+ return
69
+ end
70
+
71
+ # headers checked with signature
72
+ check_software_statement_claims(claims, request_time)
73
+ check_jwt_signature(software_statement_jwt)
74
+ end
75
+
76
+ def check_software_statement_claims(claims, request_time) # rubocop:disable Metrics/CyclomaticComplexity
77
+ unless claims['iss'] == udap_client_uri
78
+ add_message('error',
79
+ 'Registration software statement `iss` claim is incorrect: ' \
80
+ "expected '#{udap_client_uri}', got '#{claims['iss']}'")
81
+ end
82
+ unless claims['sub'] == udap_client_uri
83
+ add_message('error',
84
+ 'Registration software statement `sub` claim is incorrect: ' \
85
+ "expected '#{udap_client_uri}', got '#{claims['sub']}'")
86
+ end
87
+ unless claims['aud'] == client_registration_url
88
+ add_message('error',
89
+ 'Registration software statement `aud` claim is incorrect: ' \
90
+ "expected '#{client_registration_url}', got '#{claims['aud']}'")
91
+ end
92
+
93
+ check_software_statement_grant_types(claims)
94
+ MockUDAPServer.check_jwt_timing(claims['iat'], claims['exp'], request_time)
95
+
96
+ add_message('error', 'Registration software statement `jti` claim is missing.') unless claims['jti'].present?
97
+ unless claims['client_name'].present?
98
+ add_message('error', 'Registration software statement `client_name` claim is missing.')
99
+ end
100
+ check_software_statement_contacts(claims['contacts'])
101
+ unless claims['token_endpoint_auth_method'] == 'private_key_jwt'
102
+ add_message('error', 'Registration software statement `token_endpoint_auth_method` claim is incorrect: ' \
103
+ "expected `token_endpoint_auth_method`, got #{claims['token_endpoint_auth_method']}.")
104
+ end
105
+ add_message('error', 'Registration software statement `scope` claim is missing.') unless claims['scope'].present?
106
+
107
+ nil
108
+ end
109
+
110
+ def check_software_statement_contacts(contacts)
111
+ unless contacts.present?
112
+ add_message('error', 'Registration software statement `contacts` claim is missing.')
113
+ return
114
+ end
115
+ unless contacts.is_a?(Array)
116
+ add_message('error', 'Registration software statement `contacts` claim is missing.')
117
+ return
118
+ end
119
+ unless contacts.find { |contact| valid_uri?(contact, required_scheme: 'mailto') }.present?
120
+ add_message('error', 'Registration software statement `contacts` claim has no ' \
121
+ 'valid `mailto` uri entry.')
122
+ end
123
+
124
+ nil
125
+ end
126
+
127
+ def check_software_statement_grant_types(claims) # rubocop:disable Metrics/CyclomaticComplexity
128
+ unless claims['grant_types'].present?
129
+ add_message('error', 'Registration software statement `grant_types` claim is missing')
130
+ return
131
+ end
132
+
133
+ unless claims['grant_types'].is_a?(Array)
134
+ add_message('error', 'Registration software statement `grant_types` claim must be a list.')
135
+ return
136
+ end
137
+
138
+ has_client_credentials = claims['grant_types'].include?('client_credentials')
139
+ has_authorization_code = claims['grant_types'].include?('authorization_code')
140
+
141
+ unless has_client_credentials || has_authorization_code
142
+ add_message('error', 'Registration software statement `grant_types` claim must contain one of ' \
143
+ "'authorization_code' or 'client_credentials'")
144
+ return
145
+ end
146
+
147
+ if has_client_credentials && has_authorization_code
148
+ add_message('error', 'Registration software statement `grant_types` claim cannot contain both ' \
149
+ "'authorization_code' and 'client_credentials'")
150
+ end
151
+
152
+ extra_grants = claims['grant_types'].reject do |grant|
153
+ ['client_credentials', 'authorization_code', 'refresh_token'].include?(grant)
154
+ end
155
+ unless extra_grants.blank?
156
+ add_message('error', 'Registration software statement `grant_types` claim cannot contain values beyond ' \
157
+ "'authorization_code', 'client_credentials', and 'refresh_token")
158
+ end
159
+
160
+ check_client_credentials_software_statement(claims) if has_client_credentials
161
+ check_authorization_code_software_statement(claims) if has_authorization_code
162
+
163
+ nil
164
+ end
165
+
166
+ def check_authorization_code_software_statement(claims) # rubocop:disable Metrics/CyclomaticComplexity
167
+ if claims['redirect_uris'].blank?
168
+ add_message('error', 'Registration software statement `redirect_uris` must be present when' \
169
+ "the 'authorization_code' `grant_type` is requested.")
170
+ elsif !claims['redirect_uris'].is_a?(Array)
171
+ add_message('error', 'Registration software statement `redirect_uris` must be a list when' \
172
+ "the 'authorization_code' `grant_type` is requested.")
173
+ else
174
+ claims['redirect_uris'].each do |redirect_uri|
175
+ unless valid_uri?(redirect_uri, required_scheme: 'https')
176
+ add_message('error', "Registration software statement `redirect_uris` entry #{index + 1} is invalid: " \
177
+ 'it is not a valid https uri.')
178
+ end
179
+ end
180
+ end
181
+
182
+ if claims['logo_uri'].blank?
183
+ add_message('error', 'Registration software statement `logo_uri` must be present when' \
184
+ "the 'authorization_code' `grant_type` is requested.")
185
+ else
186
+ unless valid_uri?(claims['logo_uri'], required_scheme: 'https')
187
+ add_message('error', 'Registration software statement `logo_uri` is invalid: it is not a valid https uri.')
188
+ end
189
+ unless ['gif', 'jpg', 'jpeg', 'png'].include?(claims['logo_uri'].split['.'].last.downcase)
190
+ add_message('error', 'Registration software statement `logo_uri` is invalid: it must point to a ' \
191
+ 'PNG, JPG, or GIF file.')
192
+ end
193
+ end
194
+
195
+ if claims['response_types'].blank?
196
+ add_message('error', 'Registration software statement `response_types` must be present when' \
197
+ "the 'authorization_code' `grant_type` is requested.")
198
+ else
199
+ unless claims['response_types'].is_a?(Array) &&
200
+ claims['response_types'].size == 1 &&
201
+ claims['response_types'][0] == 'code'
202
+ add_message('error', 'Registration software statement `response_types` claim is invalid: ' \
203
+ "must contain exactly one entry with the value 'code'.")
204
+ end
205
+ end
206
+
207
+ nil
208
+ end
209
+
210
+ def check_client_credentials_software_statement(claims)
211
+ unless claims['redirect_uris'].nil?
212
+ add_message('error', 'Registration software statement `redirect_uris` must not be present when' \
213
+ "the 'client_credentials' `grant_type` is requested.")
214
+ end
215
+
216
+ unless claims['response_types'].nil?
217
+ add_message('error', 'Registration software statement `response_types` must not be present when' \
218
+ "the 'client_credentials' `grant_type` is requested.")
219
+ end
220
+
221
+ if claims['grant_types'].include?('refresh_token')
222
+ add_message('error', "Registration software statement `response_types` cannot contain 'refresh_token' when" \
223
+ "the 'client_credentials' `grant_type` is requested.")
224
+ end
225
+
226
+ nil
227
+ end
228
+
229
+ def check_jwt_signature(jwt)
230
+ error = MockUDAPServer.udap_reg_signature_verification(jwt)
231
+
232
+ return unless error.present?
233
+
234
+ add_message('error', "Signature validation failed on registration request: #{error}")
235
+ end
236
+
237
+ def valid_uri?(url, required_scheme: nil)
238
+ uri = URI.parse(url)
239
+ required_scheme.blank? || uri.scheme == required_scheme
240
+ rescue URI::InvalidURIError
241
+ false
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,178 @@
1
+ require_relative '../tags'
2
+ require_relative '../urls'
3
+ require_relative '../endpoints/mock_udap_server'
4
+
5
+ module UDAPSecurityTestKit
6
+ class UDAPClientTokenRequestVerification < Inferno::Test
7
+ include URLs
8
+
9
+ id :udap_client_token_request_verification
10
+ title 'Verify UDAP Token Requests'
11
+ description %(
12
+ Check that UDAP token requests are conformant.
13
+ )
14
+
15
+ output :udap_demonstrated
16
+ output :udap_tokens
17
+
18
+ run do
19
+ load_tagged_requests(REGISTRATION_TAG, UDAP_TAG)
20
+ output udap_demonstrated: requests.present? ? 'Yes' : 'No'
21
+ omit_if requests.blank?, 'UDAP Authentication not demonstrated as a part of this test session.'
22
+ registration_request = requests.last
23
+ registration_assertion = MockUDAPServer.parsed_request_body(registration_request)['software_statement']
24
+ registration_token =
25
+ begin
26
+ JWT::EncodedToken.new(registration_assertion)
27
+ rescue StandardError => e
28
+ assert false, "Registration request parsing failed: #{e}"
29
+ end
30
+ registered_client_id = JSON.parse(registration_request.response_body)['client_id']
31
+
32
+ requests.clear
33
+ load_tagged_requests(TOKEN_TAG, UDAP_TAG)
34
+ skip_if requests.blank?, 'No UDAP token requests made.'
35
+
36
+ jti_list = []
37
+ token_list = []
38
+ requests.each_with_index do |token_request, index|
39
+ request_params = URI.decode_www_form(token_request.request_body).to_h
40
+ check_request_params(request_params, index + 1)
41
+ check_client_assertion(request_params['client_assertion'], index + 1, jti_list, registration_token,
42
+ registered_client_id, token_request.created_at)
43
+ token_list << extract_token_from_response(token_request)
44
+ end
45
+
46
+ output udap_tokens: token_list.compact.join("\n")
47
+
48
+ assert messages.none? { |msg|
49
+ msg[:type] == 'error'
50
+ }, 'Invalid token requests detected. See messages for details.'
51
+ end
52
+
53
+ def check_request_params(params, request_num)
54
+ if params['grant_type'] != 'client_credentials'
55
+ add_message('error',
56
+ "Token request #{request_num} had an incorrect `grant_type`: expected 'client_credentials', " \
57
+ "but got '#{params['grant_type']}'")
58
+ end
59
+ if params['client_assertion_type'] != 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
60
+ add_message('error',
61
+ "Token request #{request_num} had an incorrect `client_assertion_type`: " \
62
+ "expected 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', " \
63
+ "but got '#{params['client_assertion_type']}'")
64
+ end
65
+ return unless params['udap'].to_s != '1'
66
+
67
+ add_message('error',
68
+ "Token request #{request_num} had an incorrect `udap`: " \
69
+ "expected '1', " \
70
+ "but got '#{params['udap']}'")
71
+ end
72
+
73
+ def check_client_assertion(assertion, request_num, jti_list, registration_token, registered_client_id, request_time)
74
+ decoded_token =
75
+ begin
76
+ JWT::EncodedToken.new(assertion)
77
+ rescue StandardError => e
78
+ add_message('error', "Token request #{request_num} contained an invalid client assertion jwt: #{e}")
79
+ nil
80
+ end
81
+
82
+ return unless decoded_token.present?
83
+
84
+ # header checked with signature
85
+ check_jwt_payload(decoded_token.payload, request_num, jti_list, registered_client_id, request_time)
86
+ check_jwt_signature(decoded_token, registration_token, request_num)
87
+ end
88
+
89
+ def check_jwt_payload(claims, request_num, jti_list, registered_client_id, request_time) # rubocop:disable Metrics/CyclomaticComplexity
90
+ if claims['iss'] != registered_client_id
91
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `iss` claim: " \
92
+ "expected '#{registered_client_id}', got '#{claims['iss']}'")
93
+ end
94
+
95
+ if claims['sub'] != registered_client_id
96
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `sub` claim: " \
97
+ "expected '#{registered_client_id}', got '#{claims['sub']}'")
98
+ end
99
+
100
+ if claims['aud'] != client_token_url
101
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `aud` claim: " \
102
+ "expected '#{client_token_url}', got '#{claims['aud']}'")
103
+ end
104
+
105
+ MockUDAPServer.check_jwt_timing(claims['iat'], claims['exp'], request_time)
106
+
107
+ if claims['jti'].blank?
108
+ add_message('error', "client assertion jwt on token request #{request_num} is missing the `jti` claim.")
109
+ elsif jti_list.include?(claims['jti'])
110
+ add_message('error', "client assertion jwt on token request #{request_num} has a `jti` claim that was " \
111
+ "previouly used: '#{claims['jti']}'.")
112
+ else
113
+ jti_list << claims['jti']
114
+ end
115
+
116
+ if claims['extensions'].present?
117
+ if claims['extensions'].is_a?(Hash)
118
+ check_b2b_auth_extension(claims.dig('extensions', 'hl7-b2b'), request_num)
119
+ else
120
+ add_message('error', "client assertion jwt on token request #{request_num} has an `extensions` claim that " \
121
+ 'is not a json object.')
122
+ end
123
+ else
124
+ add_message('error', "client assertion jwt on token request #{request_num} missing the `hl7-b2b` extension.")
125
+ end
126
+ end
127
+
128
+ def check_b2b_auth_extension(b2b_auth, request_num)
129
+ if b2b_auth.blank?
130
+ add_message('error', "client assertion jwt on token request #{request_num} missing the `hl7-b2b` extension.")
131
+ return
132
+ end
133
+
134
+ if b2b_auth['version'].blank?
135
+ add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} is " \
136
+ 'missing the required `version` key.')
137
+ elsif b2b_auth['version'].to_s != '1'
138
+ add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} has an " \
139
+ "incorrect `version` value: expected `1`, got #{b2b_auth['version']}.")
140
+ end
141
+
142
+ if b2b_auth['organization_id'].blank?
143
+ add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} is " \
144
+ 'missing the required `organization_id` key.')
145
+ else
146
+ begin
147
+ URI.parse(b2b_auth['organization_id'])
148
+ rescue URI::InvalidURIError
149
+ add_message('error', 'the `organization_id` key in the `hl7-b2b` extension on client assertion jwt on ' \
150
+ "token request #{request_num} is not a valid URI.")
151
+ end
152
+ end
153
+
154
+ if b2b_auth['purpose_of_use'].blank?
155
+ add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} is " \
156
+ 'missing the required `purpose_of_use` key.')
157
+ end
158
+
159
+ nil
160
+ end
161
+
162
+ def check_jwt_signature(encoded_token, registration_token, request_num)
163
+ error = MockUDAPServer.udap_token_signature_verification(encoded_token.jwt, registration_token.jwt)
164
+
165
+ return unless error.present?
166
+
167
+ add_message('error', "Signature validation failed on token request #{request_num}: #{error}")
168
+ end
169
+
170
+ def extract_token_from_response(request)
171
+ return unless request.status == 200
172
+
173
+ JSON.parse(request.response_body)&.dig('access_token')
174
+ rescue StandardError
175
+ nil
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../tags'
2
+ require_relative '../endpoints/mock_udap_server'
3
+
4
+ module UDAPSecurityTestKit
5
+ class UDAPTokenUseVerification < Inferno::Test
6
+ id :udap_client_token_use_verification
7
+ title 'Verify UDAP Token Use'
8
+ description %(
9
+ Check that a UDAP token returned to the client was used for request
10
+ authentication.
11
+ )
12
+
13
+ input :udap_demonstrated # from test :udap_client_token_request_verification based on registrations
14
+ input :udap_tokens,
15
+ optional: true
16
+
17
+ def access_request_tags
18
+ return config.options[:access_request_tags] if config.options[:access_request_tags].present?
19
+
20
+ [ACCESS_TAG]
21
+ end
22
+
23
+ run do
24
+ omit_if udap_demonstrated == 'No', 'UDAP Authentication not demonstrated as a part of this test session.'
25
+
26
+ access_requests = access_request_tags.map do |access_request_tag|
27
+ load_tagged_requests(access_request_tag).reject { |access| access.status == 401 }
28
+ end.flatten
29
+ obtained_tokens = udap_tokens&.split("\n")
30
+
31
+ skip_if obtained_tokens.blank?, 'No token requests made.'
32
+ skip_if access_requests.blank?, 'No successful access requests made.'
33
+
34
+ used_tokens = access_requests.map do |access_request|
35
+ access_request.request_headers.find do |header|
36
+ header.name.downcase == 'authorization'
37
+ end&.value&.delete_prefix('Bearer ')
38
+ end.compact
39
+
40
+ assert (used_tokens & obtained_tokens).present?, 'Returned tokens never used in any requests.'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'endpoints/mock_udap_server/registration'
2
+ require_relative 'endpoints/mock_udap_server/token'
3
+ require_relative 'endpoints/echoing_fhir_responder'
4
+ require_relative 'urls'
5
+ require_relative 'client_suite/client_registration_group'
6
+ require_relative 'client_suite/client_access_group'
7
+
8
+ module UDAPSecurityTestKit
9
+ class UDAPSecurityClientTestSuite < Inferno::TestSuite
10
+ id :udap_security_client
11
+ title 'UDAP Security Client'
12
+ description File.read(File.join(__dir__, 'docs', 'udap_client_suite_description.md'))
13
+
14
+ links [
15
+ {
16
+ type: 'source_code',
17
+ label: 'Open Source',
18
+ url: 'https://github.com/inferno-framework/udap-security-test-kit/'
19
+ },
20
+ {
21
+ type: 'report_issue',
22
+ label: 'Report Issue',
23
+ url: 'https://github.com/inferno-framework/udap-security-test-kit/issues/'
24
+ },
25
+ {
26
+ type: 'download',
27
+ label: 'Download',
28
+ url: 'https://github.com/inferno-framework/udap-security-test-kit/releases/'
29
+ },
30
+ {
31
+ type: 'ig',
32
+ label: 'Implementation Guide',
33
+ url: 'https://hl7.org/fhir/us/udap-security/STU1/'
34
+ }
35
+ ]
36
+
37
+ route(:get, UDAP_DISCOVERY_PATH, ->(_env) { MockUDAPServer.udap_server_metadata(id) })
38
+ suite_endpoint :post, REGISTRATION_PATH, MockUDAPServer::RegistrationEndpoint
39
+ suite_endpoint :post, TOKEN_PATH, MockUDAPServer::TokenEndpoint
40
+ suite_endpoint :get, FHIR_PATH, EchoingFHIRResponderEndpoint
41
+ suite_endpoint :post, FHIR_PATH, EchoingFHIRResponderEndpoint
42
+ suite_endpoint :put, FHIR_PATH, EchoingFHIRResponderEndpoint
43
+ suite_endpoint :delete, FHIR_PATH, EchoingFHIRResponderEndpoint
44
+ suite_endpoint :get, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
45
+ suite_endpoint :post, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
46
+ suite_endpoint :put, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
47
+ suite_endpoint :delete, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
48
+ suite_endpoint :get, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
49
+ suite_endpoint :post, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
50
+ suite_endpoint :put, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
51
+ suite_endpoint :delete, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
52
+ suite_endpoint :get, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
53
+ suite_endpoint :post, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
54
+ suite_endpoint :put, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
55
+ suite_endpoint :delete, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
56
+
57
+ resume_test_route :get, RESUME_PASS_PATH do |request|
58
+ request.query_parameters['token']
59
+ end
60
+
61
+ resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
62
+ request.query_parameters['token']
63
+ end
64
+
65
+ group do
66
+ title 'UDAP Client Credentials Flow'
67
+ description %(
68
+ During these tests, the client will use the UDAP Client Credentials
69
+ flow as specified in the [B2B section of the IG](https://hl7.org/fhir/us/udap-security/STU1/b2b.html)
70
+ to access a FHIR API. Clients will register, obtain an access token,
71
+ and use the access token when making a request to a FHIR API.
72
+ )
73
+
74
+ group from: :udap_client_registration
75
+ group from: :udap_client_access
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,81 @@
1
+ {
2
+ "info": {
3
+ "_postman_id": "22f52416-c6ae-4ffc-a388-54616465d149",
4
+ "name": "FHIR Request",
5
+ "description": "Make a simple FHIR request with a specific bearer token. Useful for security client tests like SMART and UDAP.\n\n- base_url: points to a running instance of inferno. Typical values will be\n \n - Inferno production: [https://inferno.healthit.gov/suites](https://inferno.healthit.gov/suites)\n \n - Inferno QA: [https://inferno-qa.healthit.gov/suites](https://inferno-qa.healthit.gov/suites)\n \n - Local docker: [http://localhost](http://localhost)\n \n - Local development: [http://localhost:4567](http://localhost:4567)",
6
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7
+ "_exporter_id": "32597978"
8
+ },
9
+ "item": [
10
+ {
11
+ "name": "Patient Read",
12
+ "request": {
13
+ "auth": {
14
+ "type": "bearer",
15
+ "bearer": [
16
+ {
17
+ "key": "token",
18
+ "value": "{{bearer_token}}",
19
+ "type": "string"
20
+ }
21
+ ]
22
+ },
23
+ "method": "GET",
24
+ "header": [],
25
+ "url": {
26
+ "raw": "{{base_url}}/custom/{{target_suite}}/fhir/Patient/example",
27
+ "host": [
28
+ "{{base_url}}"
29
+ ],
30
+ "path": [
31
+ "custom",
32
+ "{{target_suite}}",
33
+ "fhir",
34
+ "Patient",
35
+ "example"
36
+ ]
37
+ }
38
+ },
39
+ "response": []
40
+ }
41
+ ],
42
+ "event": [
43
+ {
44
+ "listen": "prerequest",
45
+ "script": {
46
+ "type": "text/javascript",
47
+ "packages": {},
48
+ "exec": [
49
+ ""
50
+ ]
51
+ }
52
+ },
53
+ {
54
+ "listen": "test",
55
+ "script": {
56
+ "type": "text/javascript",
57
+ "packages": {},
58
+ "exec": [
59
+ ""
60
+ ]
61
+ }
62
+ }
63
+ ],
64
+ "variable": [
65
+ {
66
+ "key": "base_url",
67
+ "value": "https://inferno.healthit.gov/suites",
68
+ "type": "string"
69
+ },
70
+ {
71
+ "key": "target_suite",
72
+ "value": "udap_security_client",
73
+ "type": "string"
74
+ },
75
+ {
76
+ "key": "bearer_token",
77
+ "value": "",
78
+ "type": "string"
79
+ }
80
+ ]
81
+ }