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.
- checksums.yaml +4 -4
- data/config/presets/UDAP_RunClientAgainstServer.json.erb +20 -0
- data/config/presets/UDAP_RunServerAgainstClient.json.erb +272 -0
- data/lib/udap_security_test_kit/client_credentials_token_exchange_test.rb +1 -1
- data/lib/udap_security_test_kit/client_suite/client_access_group.rb +22 -0
- data/lib/udap_security_test_kit/client_suite/client_access_interaction_test.rb +53 -0
- data/lib/udap_security_test_kit/client_suite/client_registration_group.rb +26 -0
- data/lib/udap_security_test_kit/client_suite/client_registration_interaction_test.rb +50 -0
- data/lib/udap_security_test_kit/client_suite/client_registration_verification_test.rb +244 -0
- data/lib/udap_security_test_kit/client_suite/client_token_request_verification_test.rb +178 -0
- data/lib/udap_security_test_kit/client_suite/client_token_use_verification_test.rb +43 -0
- data/lib/udap_security_test_kit/client_suite.rb +78 -0
- data/lib/udap_security_test_kit/docs/demo/FHIR Request.postman_collection.json +81 -0
- data/lib/udap_security_test_kit/docs/udap_client_suite_description.md +120 -0
- data/lib/udap_security_test_kit/endpoints/echoing_fhir_responder.rb +52 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/registration.rb +57 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/token.rb +27 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server.rb +301 -0
- data/lib/udap_security_test_kit/metadata.rb +5 -5
- data/lib/udap_security_test_kit/tags.rb +8 -0
- data/lib/udap_security_test_kit/urls.rb +45 -0
- data/lib/udap_security_test_kit/version.rb +2 -1
- data/lib/udap_security_test_kit.rb +8 -2
- 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
|
+
}
|