udap_security_test_kit 0.11.2 → 0.11.4
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/access_ac_group.rb +25 -0
- data/lib/udap_security_test_kit/client_suite/access_ac_interaction_test.rb +59 -0
- data/lib/udap_security_test_kit/client_suite/access_cc_group.rb +23 -0
- data/lib/udap_security_test_kit/client_suite/access_cc_interaction_test.rb +49 -0
- data/lib/udap_security_test_kit/client_suite/authorization_request_verification_test.rb +83 -0
- data/lib/udap_security_test_kit/client_suite/client_descriptions.rb +70 -0
- data/lib/udap_security_test_kit/client_suite/client_options.rb +20 -0
- data/lib/udap_security_test_kit/client_suite/oidc_jwks.json +32 -0
- data/lib/udap_security_test_kit/client_suite/oidc_jwks.rb +27 -0
- data/lib/udap_security_test_kit/client_suite/registration_ac_group.rb +18 -0
- data/lib/udap_security_test_kit/client_suite/registration_ac_verification_test.rb +38 -0
- data/lib/udap_security_test_kit/client_suite/registration_cc_group.rb +18 -0
- data/lib/udap_security_test_kit/client_suite/registration_cc_verification_test.rb +38 -0
- data/lib/udap_security_test_kit/client_suite/registration_interaction_test.rb +57 -0
- data/lib/udap_security_test_kit/client_suite/registration_request_verification.rb +242 -0
- data/lib/udap_security_test_kit/client_suite/token_request_ac_verification_test.rb +49 -0
- data/lib/udap_security_test_kit/client_suite/token_request_cc_verification_test.rb +49 -0
- data/lib/udap_security_test_kit/client_suite/token_request_verification.rb +223 -0
- data/lib/udap_security_test_kit/client_suite/token_use_verification_test.rb +40 -0
- data/lib/udap_security_test_kit/client_suite.rb +107 -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 +163 -0
- data/lib/udap_security_test_kit/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/authorization_endpoint.rb +28 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/registration_endpoint.rb +31 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/token_endpoint.rb +56 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_authorization_response_creation.rb +63 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_registration_response_creation.rb +28 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_token_response_creation.rb +218 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server.rb +382 -0
- data/lib/udap_security_test_kit/metadata.rb +4 -4
- data/lib/udap_security_test_kit/tags.rb +12 -0
- data/lib/udap_security_test_kit/urls.rb +52 -0
- data/lib/udap_security_test_kit/version.rb +2 -2
- data/lib/udap_security_test_kit.rb +8 -2
- metadata +36 -2
@@ -0,0 +1,223 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
module TokenRequestVerification
|
3
|
+
def verify_token_requests(oauth_flow)
|
4
|
+
registration_token =
|
5
|
+
begin
|
6
|
+
JWT::EncodedToken.new(udap_registration_jwt)
|
7
|
+
rescue StandardError => e
|
8
|
+
assert false, "Registration request parsing failed: #{e}"
|
9
|
+
end
|
10
|
+
|
11
|
+
jti_list = []
|
12
|
+
token_list = []
|
13
|
+
requests.each_with_index do |token_request, index|
|
14
|
+
request_params = URI.decode_www_form(token_request.request_body).to_h
|
15
|
+
if request_params['grant_type'] == 'refresh_token'
|
16
|
+
check_refresh_request_params(oauth_flow, request_params, index + 1)
|
17
|
+
else
|
18
|
+
check_request_params(oauth_flow, request_params, index + 1)
|
19
|
+
end
|
20
|
+
check_client_assertion(oauth_flow, request_params['client_assertion'], index + 1, jti_list, registration_token,
|
21
|
+
client_id, token_request.created_at)
|
22
|
+
token_list << extract_token_from_response(token_request)
|
23
|
+
end
|
24
|
+
|
25
|
+
output udap_tokens: token_list.compact.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
def check_request_params(oauth_flow, params, request_num)
|
29
|
+
if params['grant_type'] != oauth_flow
|
30
|
+
add_message('error',
|
31
|
+
"Token request #{request_num} had an incorrect `grant_type`: expected '#{oauth_flow}', " \
|
32
|
+
"but got '#{params['grant_type']}'")
|
33
|
+
end
|
34
|
+
if params['client_assertion_type'] != 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
|
35
|
+
add_message('error',
|
36
|
+
"Token request #{request_num} had an incorrect `client_assertion_type`: " \
|
37
|
+
"expected 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', " \
|
38
|
+
"but got '#{params['client_assertion_type']}'")
|
39
|
+
end
|
40
|
+
unless params['udap'].to_s == '1'
|
41
|
+
add_message('error',
|
42
|
+
"Token request #{request_num} had an incorrect `udap`: " \
|
43
|
+
"expected '1', " \
|
44
|
+
"but got '#{params['udap']}'")
|
45
|
+
end
|
46
|
+
|
47
|
+
check_authorization_code_request_params(params, request_num) if oauth_flow == AUTHORIZATION_CODE_TAG
|
48
|
+
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_authorization_code_request_params(params, request_num)
|
53
|
+
if params['code'].present?
|
54
|
+
|
55
|
+
authorization_request = MockUDAPServer.authorization_request_for_code(params['code'], test_session_id)
|
56
|
+
|
57
|
+
if authorization_request.present?
|
58
|
+
authorization_body = MockUDAPServer.authorization_code_request_details(authorization_request)
|
59
|
+
|
60
|
+
if params['redirect_uri'] != authorization_body['redirect_uri']
|
61
|
+
add_message('error', "Authorization code token request #{request_num} included an incorrect " \
|
62
|
+
"`redirect_uri` value: expected '#{authorization_body['redirect_uri']} " \
|
63
|
+
"but got '#{params['redirect_uri']}'")
|
64
|
+
end
|
65
|
+
|
66
|
+
return unless params['code_verifier'].present? # optional in UDAP
|
67
|
+
|
68
|
+
pkce_error = MockUDAPServer.pkce_error(params['code_verifier'],
|
69
|
+
authorization_body['code_challenge'],
|
70
|
+
authorization_body['code_challenge_method'])
|
71
|
+
if pkce_error.present?
|
72
|
+
add_message('error', 'Error performing pkce verification on the `code_verifier` value in ' \
|
73
|
+
"authorization code token request #{request_num}: #{pkce_error}")
|
74
|
+
end
|
75
|
+
else
|
76
|
+
add_message('error', "Authorization code token request #{request_num} included a code not " \
|
77
|
+
"issued during this test session: '#{params['code']}'")
|
78
|
+
end
|
79
|
+
else
|
80
|
+
add_message('error', "Authorization code token request #{request_num} missing a `code`")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_client_assertion(oauth_flow, assertion, request_num, jti_list, registration_token, registered_client_id,
|
85
|
+
request_time)
|
86
|
+
decoded_token =
|
87
|
+
begin
|
88
|
+
JWT::EncodedToken.new(assertion)
|
89
|
+
rescue StandardError => e
|
90
|
+
add_message('error', "Token request #{request_num} contained an invalid client assertion jwt: #{e}")
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
return unless decoded_token.present?
|
95
|
+
|
96
|
+
# header checked with signature
|
97
|
+
check_jwt_payload(oauth_flow, decoded_token.payload, request_num, jti_list, registered_client_id, request_time)
|
98
|
+
check_jwt_signature(decoded_token, registration_token, request_num)
|
99
|
+
end
|
100
|
+
|
101
|
+
def check_jwt_payload(oauth_flow, claims, request_num, jti_list, registered_client_id, request_time) # rubocop:disable Metrics/CyclomaticComplexity
|
102
|
+
if claims['iss'] != registered_client_id
|
103
|
+
add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `iss` claim: " \
|
104
|
+
"expected '#{registered_client_id}', got '#{claims['iss']}'")
|
105
|
+
end
|
106
|
+
|
107
|
+
if claims['sub'] != registered_client_id
|
108
|
+
add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `sub` claim: " \
|
109
|
+
"expected '#{registered_client_id}', got '#{claims['sub']}'")
|
110
|
+
end
|
111
|
+
|
112
|
+
if claims['aud'] != client_token_url
|
113
|
+
add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `aud` claim: " \
|
114
|
+
"expected '#{client_token_url}', got '#{claims['aud']}'")
|
115
|
+
end
|
116
|
+
|
117
|
+
MockUDAPServer.check_jwt_timing(claims['iat'], claims['exp'], request_time)
|
118
|
+
|
119
|
+
if claims['jti'].blank?
|
120
|
+
add_message('error', "client assertion jwt on token request #{request_num} is missing the `jti` claim.")
|
121
|
+
elsif jti_list.include?(claims['jti'])
|
122
|
+
add_message('error', "client assertion jwt on token request #{request_num} has a `jti` claim that was " \
|
123
|
+
"previouly used: '#{claims['jti']}'.")
|
124
|
+
else
|
125
|
+
jti_list << claims['jti']
|
126
|
+
end
|
127
|
+
|
128
|
+
return unless oauth_flow == CLIENT_CREDENTIALS_TAG
|
129
|
+
|
130
|
+
if claims['extensions'].present?
|
131
|
+
if claims['extensions'].is_a?(Hash)
|
132
|
+
check_b2b_auth_extension(claims.dig('extensions', 'hl7-b2b'), request_num)
|
133
|
+
else
|
134
|
+
add_message('error', "client assertion jwt on token request #{request_num} has an `extensions` claim that " \
|
135
|
+
'is not a json object.')
|
136
|
+
end
|
137
|
+
else
|
138
|
+
add_message('error', "client assertion jwt on token request #{request_num} missing the `hl7-b2b` extension.")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def check_b2b_auth_extension(b2b_auth, request_num)
|
143
|
+
if b2b_auth.blank?
|
144
|
+
add_message('error', "client assertion jwt on token request #{request_num} missing the `hl7-b2b` extension.")
|
145
|
+
return
|
146
|
+
end
|
147
|
+
|
148
|
+
if b2b_auth['version'].blank?
|
149
|
+
add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} is " \
|
150
|
+
'missing the required `version` key.')
|
151
|
+
elsif b2b_auth['version'].to_s != '1'
|
152
|
+
add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} has an " \
|
153
|
+
"incorrect `version` value: expected `1`, got #{b2b_auth['version']}.")
|
154
|
+
end
|
155
|
+
|
156
|
+
if b2b_auth['organization_id'].blank?
|
157
|
+
add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} is " \
|
158
|
+
'missing the required `organization_id` key.')
|
159
|
+
else
|
160
|
+
begin
|
161
|
+
URI.parse(b2b_auth['organization_id'])
|
162
|
+
rescue URI::InvalidURIError
|
163
|
+
add_message('error', 'the `organization_id` key in the `hl7-b2b` extension on client assertion jwt on ' \
|
164
|
+
"token request #{request_num} is not a valid URI.")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
if b2b_auth['purpose_of_use'].blank?
|
169
|
+
add_message('error', "the `hl7-b2b` extension on client assertion jwt on token request #{request_num} is " \
|
170
|
+
'missing the required `purpose_of_use` key.')
|
171
|
+
end
|
172
|
+
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
def check_jwt_signature(encoded_token, registration_token, request_num)
|
177
|
+
error = MockUDAPServer.udap_token_signature_verification(encoded_token.jwt, registration_token.jwt)
|
178
|
+
|
179
|
+
return unless error.present?
|
180
|
+
|
181
|
+
add_message('error', "Signature validation failed on token request #{request_num}: #{error}")
|
182
|
+
end
|
183
|
+
|
184
|
+
def extract_token_from_response(request)
|
185
|
+
return unless request.status == 200
|
186
|
+
|
187
|
+
JSON.parse(request.response_body)&.dig('access_token')
|
188
|
+
rescue StandardError
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def check_refresh_request_params(oauth_flow, params, request_num)
|
193
|
+
if oauth_flow == CLIENT_CREDENTIALS_TAG
|
194
|
+
add_message('error',
|
195
|
+
"Invalid refresh request #{request_num} found during client_credentials flow.")
|
196
|
+
return
|
197
|
+
end
|
198
|
+
|
199
|
+
if params['grant_type'] != 'refresh_token'
|
200
|
+
add_message('error',
|
201
|
+
"Refresh request #{request_num} had an incorrect `grant_type`: expected 'refresh_token', " \
|
202
|
+
"but got '#{params['grant_type']}'")
|
203
|
+
end
|
204
|
+
if params['client_assertion_type'] != 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
|
205
|
+
add_message('error',
|
206
|
+
"Token refresh request #{request_num} had an incorrect " \
|
207
|
+
"`client_assertion_type`: expected 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', " \
|
208
|
+
"but got '#{params['client_assertion_type']}'")
|
209
|
+
end
|
210
|
+
|
211
|
+
authorization_code = MockUDAPServer.refresh_token_to_authorization_code(params['refresh_token'])
|
212
|
+
authorization_request = MockUDAPServer.authorization_request_for_code(authorization_code, test_session_id)
|
213
|
+
if authorization_request.present?
|
214
|
+
# TODO: - check that the scope is a subset of the original authorization code request
|
215
|
+
else
|
216
|
+
add_message('error', "Authorization code token refresh request #{request_num} included a refresh token not " \
|
217
|
+
"issued during this test session: '#{params['refresh_token']}'")
|
218
|
+
end
|
219
|
+
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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_tokens,
|
14
|
+
optional: true
|
15
|
+
|
16
|
+
def access_request_tags
|
17
|
+
return config.options[:access_request_tags] if config.options[:access_request_tags].present?
|
18
|
+
|
19
|
+
[ACCESS_TAG]
|
20
|
+
end
|
21
|
+
|
22
|
+
run do
|
23
|
+
access_requests = access_request_tags.map do |access_request_tag|
|
24
|
+
load_tagged_requests(access_request_tag).reject { |access| access.status == 401 }
|
25
|
+
end.flatten
|
26
|
+
obtained_tokens = udap_tokens&.split("\n")
|
27
|
+
|
28
|
+
skip_if obtained_tokens.blank?, 'No token requests made.'
|
29
|
+
skip_if access_requests.blank?, 'No successful access requests made.'
|
30
|
+
|
31
|
+
used_tokens = access_requests.map do |access_request|
|
32
|
+
access_request.request_headers.find do |header|
|
33
|
+
header.name.downcase == 'authorization'
|
34
|
+
end&.value&.delete_prefix('Bearer ')
|
35
|
+
end.compact
|
36
|
+
|
37
|
+
assert (used_tokens & obtained_tokens).present?, 'Returned tokens never used in any requests.'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative 'endpoints/mock_udap_server/registration_endpoint'
|
2
|
+
require_relative 'endpoints/mock_udap_server/authorization_endpoint'
|
3
|
+
require_relative 'endpoints/mock_udap_server/token_endpoint'
|
4
|
+
require_relative 'endpoints/echoing_fhir_responder_endpoint'
|
5
|
+
require_relative 'urls'
|
6
|
+
require_relative 'client_suite/registration_ac_group'
|
7
|
+
require_relative 'client_suite/registration_cc_group'
|
8
|
+
require_relative 'client_suite/access_ac_group'
|
9
|
+
require_relative 'client_suite/access_cc_group'
|
10
|
+
|
11
|
+
module UDAPSecurityTestKit
|
12
|
+
class UDAPSecurityClientTestSuite < Inferno::TestSuite
|
13
|
+
id :udap_security_client
|
14
|
+
title 'UDAP Security Client'
|
15
|
+
description File.read(File.join(__dir__, 'docs', 'udap_client_suite_description.md'))
|
16
|
+
|
17
|
+
links [
|
18
|
+
{
|
19
|
+
type: 'source_code',
|
20
|
+
label: 'Open Source',
|
21
|
+
url: 'https://github.com/inferno-framework/udap-security-test-kit/'
|
22
|
+
},
|
23
|
+
{
|
24
|
+
type: 'report_issue',
|
25
|
+
label: 'Report Issue',
|
26
|
+
url: 'https://github.com/inferno-framework/udap-security-test-kit/issues/'
|
27
|
+
},
|
28
|
+
{
|
29
|
+
type: 'download',
|
30
|
+
label: 'Download',
|
31
|
+
url: 'https://github.com/inferno-framework/udap-security-test-kit/releases/'
|
32
|
+
},
|
33
|
+
{
|
34
|
+
type: 'ig',
|
35
|
+
label: 'Implementation Guide',
|
36
|
+
url: 'https://hl7.org/fhir/us/udap-security/STU1/'
|
37
|
+
}
|
38
|
+
]
|
39
|
+
|
40
|
+
suite_option :client_type,
|
41
|
+
title: 'UDAP Client Type',
|
42
|
+
list_options: [
|
43
|
+
{
|
44
|
+
label: 'UDAP Authorization Code Client',
|
45
|
+
value: UDAPClientOptions::UDAP_AUTHORIZATION_CODE
|
46
|
+
},
|
47
|
+
{
|
48
|
+
label: 'UDAP Client Credentials Client',
|
49
|
+
value: UDAPClientOptions::UDAP_CLIENT_CREDENTIALS
|
50
|
+
}
|
51
|
+
]
|
52
|
+
|
53
|
+
route(:get, UDAP_DISCOVERY_PATH, ->(_env) { MockUDAPServer.udap_server_metadata(id) })
|
54
|
+
route(:get, OIDC_DISCOVERY_PATH, ->(_env) { MockUDAPServer.openid_connect_metadata(id) })
|
55
|
+
route(
|
56
|
+
:get,
|
57
|
+
OIDC_JWKS_PATH,
|
58
|
+
->(_env) { [200, { 'Content-Type' => 'application/json' }, [OIDCJWKS.jwks_json]] }
|
59
|
+
)
|
60
|
+
|
61
|
+
suite_endpoint :post, REGISTRATION_PATH, MockUDAPServer::RegistrationEndpoint
|
62
|
+
suite_endpoint :get, AUTHORIZATION_PATH, MockUDAPServer::AuthorizationEndpoint
|
63
|
+
suite_endpoint :post, AUTHORIZATION_PATH, MockUDAPServer::AuthorizationEndpoint
|
64
|
+
suite_endpoint :post, TOKEN_PATH, MockUDAPServer::TokenEndpoint
|
65
|
+
suite_endpoint :get, FHIR_PATH, EchoingFHIRResponderEndpoint
|
66
|
+
suite_endpoint :post, FHIR_PATH, EchoingFHIRResponderEndpoint
|
67
|
+
suite_endpoint :put, FHIR_PATH, EchoingFHIRResponderEndpoint
|
68
|
+
suite_endpoint :delete, FHIR_PATH, EchoingFHIRResponderEndpoint
|
69
|
+
suite_endpoint :get, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
|
70
|
+
suite_endpoint :post, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
|
71
|
+
suite_endpoint :put, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
|
72
|
+
suite_endpoint :delete, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
|
73
|
+
suite_endpoint :get, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
|
74
|
+
suite_endpoint :post, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
|
75
|
+
suite_endpoint :put, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
|
76
|
+
suite_endpoint :delete, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
|
77
|
+
suite_endpoint :get, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
|
78
|
+
suite_endpoint :post, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
|
79
|
+
suite_endpoint :put, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
|
80
|
+
suite_endpoint :delete, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
|
81
|
+
|
82
|
+
resume_test_route :get, RESUME_PASS_PATH do |request|
|
83
|
+
request.query_parameters['token']
|
84
|
+
end
|
85
|
+
|
86
|
+
resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
|
87
|
+
request.query_parameters['token']
|
88
|
+
end
|
89
|
+
|
90
|
+
group from: :udap_client_registration_ac,
|
91
|
+
required_suite_options: {
|
92
|
+
client_type: UDAPClientOptions::UDAP_AUTHORIZATION_CODE
|
93
|
+
}
|
94
|
+
group from: :udap_client_registration_cc,
|
95
|
+
required_suite_options: {
|
96
|
+
client_type: UDAPClientOptions::UDAP_CLIENT_CREDENTIALS
|
97
|
+
}
|
98
|
+
group from: :udap_client_access_ac,
|
99
|
+
required_suite_options: {
|
100
|
+
client_type: UDAPClientOptions::UDAP_AUTHORIZATION_CODE
|
101
|
+
}
|
102
|
+
group from: :udap_client_access_cc,
|
103
|
+
required_suite_options: {
|
104
|
+
client_type: UDAPClientOptions::UDAP_CLIENT_CREDENTIALS
|
105
|
+
}
|
106
|
+
end
|
107
|
+
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
|
+
}
|
@@ -0,0 +1,163 @@
|
|
1
|
+
## Overview
|
2
|
+
|
3
|
+
The UDAP Security Client Test Suite verifies the conformance of
|
4
|
+
client systems to the STU 1.0.0 version of the HL7® FHIR®
|
5
|
+
[Security for Scalable Registration, Authentication, and Authorization (UDAP Security) FHIR IG](https://hl7.org/fhir/us/udap-security/STU1/).
|
6
|
+
|
7
|
+
## Scope
|
8
|
+
|
9
|
+
The UDAP Security Client Test Suite verifies that systems correctly implement
|
10
|
+
the [UDAP Security IG](https://hl7.org/fhir/us/udap-security/STU1/)
|
11
|
+
for authorizating and/or authenticating with a server in order to gain
|
12
|
+
access to HL7® FHIR® APIs. The suite contains options for testing clients that follow the
|
13
|
+
- Authorization Code flow for [consumer facing](https://hl7.org/fhir/us/udap-security/STU1/consumer.html)
|
14
|
+
or [Business-to-Business](https://hl7.org/fhir/us/udap-security/STU1/b2b.html) access.
|
15
|
+
- Client Credentials flow for [Business-to-Business](https://hl7.org/fhir/us/udap-security/STU1/b2b.html)
|
16
|
+
access.
|
17
|
+
|
18
|
+
These tests are a **DRAFT** intended to allow implementers to perform
|
19
|
+
preliminary checks of their systems against UDAP Security IG
|
20
|
+
and [provide feedback](https://github.com/inferno-framework/udap-security-test-kit/issues)
|
21
|
+
on the tests. Future versions of these tests may verify other
|
22
|
+
requirements and may change the test verification logic.
|
23
|
+
|
24
|
+
## Test Methodology
|
25
|
+
|
26
|
+
For these tests Inferno simulates a UDAP server that supports both the authorization code
|
27
|
+
and the client credentials flows. In both cases, testers will
|
28
|
+
1. Provide to Inferno the client URI with which they will register their system.
|
29
|
+
2. Make a dynamic registration request to Inferno using the provided client URI
|
30
|
+
and including the X.509 certificate used to sign the registeration and subsequent
|
31
|
+
token requests which must also have the client URI as a Subject Alternative Name (SAN)
|
32
|
+
value in the certificate.
|
33
|
+
3. Obtain an access token with a request using the client Id returned during registration
|
34
|
+
and signed using same X.509 certificate supplied during registration.
|
35
|
+
4. Use that access token on a FHIR API request.
|
36
|
+
|
37
|
+
The simulated UDAP server is relatively permissive in the sense that it will often
|
38
|
+
provide successful responses even when the request is not conformant. When
|
39
|
+
requesting tokens, Inferno will return an access token as long as it can find
|
40
|
+
the client id and the signature is valid. This allows incomplete systems to
|
41
|
+
run the tests. However, these non-conformant requests will be flagged by
|
42
|
+
the tests as failures so that systems will not pass the tests without being
|
43
|
+
fully conformant.
|
44
|
+
|
45
|
+
## Running the Tests
|
46
|
+
|
47
|
+
### Quick Start
|
48
|
+
|
49
|
+
The following inputs must be provided by the tester at a minimum to execute
|
50
|
+
any tests in this suite:
|
51
|
+
1. **UDAP Client URI**: The UDAP Client URI that will be used to register with
|
52
|
+
Inferno's simulated UDAP server.
|
53
|
+
|
54
|
+
The *Additional Inputs* section below describes options available to customize
|
55
|
+
the behavior of Inferno's server simulation.
|
56
|
+
|
57
|
+
### Demonstration
|
58
|
+
|
59
|
+
To try out these tests without a UDAP client implementation, these tests can be exercised
|
60
|
+
using the UDAP Security server test suite and a simple HTTP request generator. The following
|
61
|
+
steps use [Postman](https://www.postman.com/) to generate the access request using
|
62
|
+
[this collection](https://github.com/inferno-framework/udap-security-test-kit/blob/main/lib/udap_security_test_kit/docs/demo/FHIR%20Request.postman_collection.json).
|
63
|
+
Install the app and import the collection before following these steps.
|
64
|
+
|
65
|
+
1. Start an instance of the UDAP Security Client test suite and choose either option: *Authorization Code*,
|
66
|
+
or *Client Credentials* flow. Remember your choice for use later.
|
67
|
+
1. From the drop down in the upper left, select preset "Demo: Run Against the UDAP Security Server Suite".
|
68
|
+
1. Click the "RUN ALL TESTS" button in the upper right and click "SUBMIT"
|
69
|
+
1. In a new tab, start an instance of the UDAP Security Server Test Suite
|
70
|
+
1. From the drop down in the upper left, select preset "Demo: Run Against the UDAP Security Client Suite"
|
71
|
+
1. Select the test group corresponding to your choice in step 1: **1** for *Authorization Code* and **2**
|
72
|
+
for *Client Credentials*. Click the "RUN ALL TESTS" button in the upper right, and click "SUBMIT".
|
73
|
+
1. If testing the *Authorization Code* flow, click the link to authorize with the server.
|
74
|
+
1. In the Client suite tab, click the link in the wait dialog to continue the tests.
|
75
|
+
1. In the Server suite tab, find the access token to use for the data access request by opening
|
76
|
+
test **1.3.03** or **2.3.01** OAuth token exchange request succeeds when supplied correct information,
|
77
|
+
click on the "REQUESTS" tab, clicking on the "DETAILS" button, and expanding the "Response Body".
|
78
|
+
Copy the "access_token" value, which will be a ~100 character string of letters and numbers (e.g., eyJjbGllbnRfaWQiOiJzbWFydF9jbGllbnRfdGVzdF9kZW1vIiwiZXhwaXJhdGlvbiI6MTc0MzUxNDk4Mywibm9uY2UiOiJlZDI5MWIwNmZhMTE4OTc4In0)
|
79
|
+
1. Open Postman and open the "FHIR Request" Collection. Click the "Variables" tab and add the
|
80
|
+
copied access token as the current value of the `bearer_token` variable. Also update the
|
81
|
+
`base_url` value for where the test is running (see details on the "Overview" tab).
|
82
|
+
Save the collection.
|
83
|
+
1. Select the "Patient Read" request and click "Send". A FHIR Patient resource should be returned.
|
84
|
+
1. Return to the client tests and click the link to continue and complete the tests.
|
85
|
+
|
86
|
+
The client tests should pass. On the server side some of the registration tests will fail. This is
|
87
|
+
expected as the Server tests make several intentionally invalid token requests. Inferno's simulated UDAP
|
88
|
+
server responds successfully to those requests when the client id can be identified, but flags them as
|
89
|
+
not conformant causing these expected failures. Because responding successfully to non-conformant
|
90
|
+
registration requests is itself not conformant there are corresponding failures on the server test.
|
91
|
+
|
92
|
+
### Additional Inputs
|
93
|
+
|
94
|
+
#### Inputs Controlling Token Responses
|
95
|
+
|
96
|
+
The UDAP simulation incorporates some aspects of SMART App launch including its approach to
|
97
|
+
[context data](https://hl7.org/fhir/smart-app-launch/STU2.2/scopes-and-launch-context.html)
|
98
|
+
in token responses during the authorization code flow.
|
99
|
+
Inferno's SMART simulation does not include the details needed to populate
|
100
|
+
these details into the token response when requested by apps using scopes.
|
101
|
+
If the tested app needs and will request these details, the tester must provide them for Inferno
|
102
|
+
to respond with using the following inputs:
|
103
|
+
- **Launch Context** (available for all *SMART App Launch* clients): Testers can provide a JSON
|
104
|
+
array for Inferno to use as the base for building a token response on. This can include
|
105
|
+
keys like `"patient"` when the `launch/patient` scope will be requested. Note that when keys that Inferno
|
106
|
+
also populates (e.g. `access_token` or `id_token`) are included, the Inferno value will be returned.
|
107
|
+
- **FHIR User Relative Reference** (available for all *SMART App Launch* clients): Testers
|
108
|
+
can provide a FHIR relative reference (`<resource type>/<id>`) for the FHIR user record
|
109
|
+
to return with the `id_token` when the `openid` and `fhirUser` scopes are requested. If populated,
|
110
|
+
include the corresponding resource in the **Available Resources** input (See the "Inputs
|
111
|
+
Controlling FHIR Responses" section) so that it can be accessed via FHIR read.
|
112
|
+
|
113
|
+
#### Inputs Controlling FHIR Responses
|
114
|
+
The focus of this test kit is on the auth protocol, so the simulated FHIR server implemented
|
115
|
+
in this test suite is very simple. It will respond to any FHIR request with either:
|
116
|
+
- A resource from a tester-provided Bundle in the **Available Resources** input
|
117
|
+
if the request is a read matching a resource type and id found in the Bundle.
|
118
|
+
- Otherwise, the contents of the **Default FHIR Response** input, if provided.
|
119
|
+
- Otherwise, an OperationOutcome indicating no response was available.
|
120
|
+
|
121
|
+
The two inputs that control these response include:
|
122
|
+
- **Available Resources**: A FHIR Bundle of resources to make available via the
|
123
|
+
simulated FHIR sever. Each entry must contain a resource with the id element
|
124
|
+
populated. Each instance present will be available for retrieval from Inferno
|
125
|
+
at the endpoint: `<fhir-base>/<resource type>/<instance id>`. These will only
|
126
|
+
be available through the read interaction.
|
127
|
+
- **FHIR Response to Echo**: A static FHIR JSON body for Inferno to return for all FHIR requests
|
128
|
+
not covered by reads of instances in the **Available Resources** input. In this case,
|
129
|
+
the simulation is a simple echo and Inferno does not check that the response is
|
130
|
+
appropriate for the request made.
|
131
|
+
|
132
|
+
## Current Limitations
|
133
|
+
|
134
|
+
This test kit is still in draft form and does not test all of the requirements and features
|
135
|
+
described in the UDAP Security IG for clients.
|
136
|
+
|
137
|
+
The following sections list known gaps and limitations.
|
138
|
+
|
139
|
+
### SMART Scope Checking and Fulfilment
|
140
|
+
|
141
|
+
These tests do not verify any details about scopes, including that the
|
142
|
+
- Requested scopes are conformant, such as that they have a valid format and are consistent
|
143
|
+
between authorization and refresh token requests.
|
144
|
+
- Provided **Launch Context** input fullfils the requested data context scopes.
|
145
|
+
- Access performed is allowed by the requested scope.
|
146
|
+
|
147
|
+
### UDAP Server Simulation Limitations
|
148
|
+
|
149
|
+
This test suite contains a simulation of a UDAP server which is not fully
|
150
|
+
general and not all conformant clients may be able to interact with it. One
|
151
|
+
specific example is that the UDAP configuration metadata available at
|
152
|
+
`.well-known/udap` for the simulated server is fixed and cannot be changed by
|
153
|
+
testers at this time. Despite the current limitations, the intention is for Inferno to
|
154
|
+
support a variety of conformant choices, so please report issues that prevent conformant
|
155
|
+
systems from passing in the [github repository's issues page](https://github.com/inferno-framework/udap-security-test-kit/issues/).
|
156
|
+
|
157
|
+
### FHIR Server Simulation Limitations
|
158
|
+
|
159
|
+
The FHIR server simulation used to support clients in demonstrating their ability to access
|
160
|
+
FHIR APIs using access tokens obtained using the SMART flows is very limited. Testers are currently
|
161
|
+
able to provide a list of resources to be read and a single static response that will be echoed for any
|
162
|
+
other FHIR request made. While Inferno will never implement a fully general FHIR server simulation,
|
163
|
+
additional options, such as may be added in the future based on community feedback.
|