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.
Files changed (40) 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/access_ac_group.rb +25 -0
  6. data/lib/udap_security_test_kit/client_suite/access_ac_interaction_test.rb +59 -0
  7. data/lib/udap_security_test_kit/client_suite/access_cc_group.rb +23 -0
  8. data/lib/udap_security_test_kit/client_suite/access_cc_interaction_test.rb +49 -0
  9. data/lib/udap_security_test_kit/client_suite/authorization_request_verification_test.rb +83 -0
  10. data/lib/udap_security_test_kit/client_suite/client_descriptions.rb +70 -0
  11. data/lib/udap_security_test_kit/client_suite/client_options.rb +20 -0
  12. data/lib/udap_security_test_kit/client_suite/oidc_jwks.json +32 -0
  13. data/lib/udap_security_test_kit/client_suite/oidc_jwks.rb +27 -0
  14. data/lib/udap_security_test_kit/client_suite/registration_ac_group.rb +18 -0
  15. data/lib/udap_security_test_kit/client_suite/registration_ac_verification_test.rb +38 -0
  16. data/lib/udap_security_test_kit/client_suite/registration_cc_group.rb +18 -0
  17. data/lib/udap_security_test_kit/client_suite/registration_cc_verification_test.rb +38 -0
  18. data/lib/udap_security_test_kit/client_suite/registration_interaction_test.rb +57 -0
  19. data/lib/udap_security_test_kit/client_suite/registration_request_verification.rb +242 -0
  20. data/lib/udap_security_test_kit/client_suite/token_request_ac_verification_test.rb +49 -0
  21. data/lib/udap_security_test_kit/client_suite/token_request_cc_verification_test.rb +49 -0
  22. data/lib/udap_security_test_kit/client_suite/token_request_verification.rb +223 -0
  23. data/lib/udap_security_test_kit/client_suite/token_use_verification_test.rb +40 -0
  24. data/lib/udap_security_test_kit/client_suite.rb +107 -0
  25. data/lib/udap_security_test_kit/docs/demo/FHIR Request.postman_collection.json +81 -0
  26. data/lib/udap_security_test_kit/docs/udap_client_suite_description.md +163 -0
  27. data/lib/udap_security_test_kit/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
  28. data/lib/udap_security_test_kit/endpoints/mock_udap_server/authorization_endpoint.rb +28 -0
  29. data/lib/udap_security_test_kit/endpoints/mock_udap_server/registration_endpoint.rb +31 -0
  30. data/lib/udap_security_test_kit/endpoints/mock_udap_server/token_endpoint.rb +56 -0
  31. data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_authorization_response_creation.rb +63 -0
  32. data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_registration_response_creation.rb +28 -0
  33. data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_token_response_creation.rb +218 -0
  34. data/lib/udap_security_test_kit/endpoints/mock_udap_server.rb +382 -0
  35. data/lib/udap_security_test_kit/metadata.rb +4 -4
  36. data/lib/udap_security_test_kit/tags.rb +12 -0
  37. data/lib/udap_security_test_kit/urls.rb +52 -0
  38. data/lib/udap_security_test_kit/version.rb +2 -2
  39. data/lib/udap_security_test_kit.rb +8 -2
  40. 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.