smart_udap_harmonization_test_kit 0.9.0
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 +7 -0
- data/LICENSE +201 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_authorization_code_authentication_group.rb +50 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_authorization_code_group.rb +127 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_authorization_code_redirect_test.rb +88 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_context_test.rb +94 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_encounter_context_test.rb +59 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_fhir_context_test.rb +87 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_intent_context_test.rb +25 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_launch_context_group.rb +86 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_need_patient_banner_context_test.rb +25 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_openid_connect_group.rb +66 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_patient_context_test.rb +58 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_request_builder.rb +27 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_smart_style_url_context_test.rb +33 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_tenant_context_test.rb +25 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_token_refresh_test.rb +129 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_token_refresh_with_scopes_group.rb +81 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_token_refresh_without_scopes_group.rb +70 -0
- data/lib/smart_udap_harmonization_test_kit/smart_udap_token_response_scope_test.rb +56 -0
- data/lib/smart_udap_harmonization_test_kit/version.rb +5 -0
- data/lib/smart_udap_harmonization_test_kit.rb +73 -0
- metadata +109 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'smart_udap_context_test'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_FHIR_ContextTest < SMART_UDAP_ContextTest # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_fhir_context
|
6
|
+
title 'Support for "fhirContext" launch context'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This test validates the presence and format of the "fhirContext"
|
9
|
+
launch context.
|
10
|
+
DESCRIPTION
|
11
|
+
|
12
|
+
def context_field_name
|
13
|
+
'fhirContext'
|
14
|
+
end
|
15
|
+
|
16
|
+
def context_scopes
|
17
|
+
[].freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_context_field # rubocop:disable Metrics/CyclomaticComplexity
|
21
|
+
assert context_field.is_a?(Array),
|
22
|
+
"`fhirContext` field should be an Array, but found `#{context_field.class.name}`"
|
23
|
+
|
24
|
+
context_field_types = context_field.map(&:class).uniq
|
25
|
+
|
26
|
+
assert context_field_types.length == 1,
|
27
|
+
"Inconsistent `fhirContext` types found: #{context_field_types.map(&:name).join(', ')}"
|
28
|
+
|
29
|
+
if context_field.any? { |member| member.is_a? String }
|
30
|
+
assert context_field.none? { |member| member&.start_with?('Patient/') || member&.start_with?('Encounter/') },
|
31
|
+
'Patient and Encounter references are not permitted within fhirContext in SMART App Launch 2.0.0'
|
32
|
+
|
33
|
+
pass
|
34
|
+
end
|
35
|
+
|
36
|
+
non_hash_fields = context_field.reject { |member| member.is_a? Hash }
|
37
|
+
non_hash_fields_string = non_hash_fields.map { |member| "`#{member.class.name}`" }.join(', ')
|
38
|
+
assert non_hash_fields.empty?,
|
39
|
+
"All `fhirContext` elements should be JSON objects, but found #{non_hash_fields_string}"
|
40
|
+
|
41
|
+
field_types = {
|
42
|
+
'reference' => String,
|
43
|
+
'canonical' => String,
|
44
|
+
'identifier' => Hash,
|
45
|
+
'type' => String,
|
46
|
+
'role' => String
|
47
|
+
}
|
48
|
+
|
49
|
+
bad_fields = []
|
50
|
+
context_field.each do |member|
|
51
|
+
field_types.each do |name, type|
|
52
|
+
if member.key?(name) && !member[name].is_a?(type)
|
53
|
+
bad_fields << { name:, type: member[name].class.name, expected_type: type.name }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
bad_types_list =
|
59
|
+
bad_fields.map do |bad_field|
|
60
|
+
"\n* `#{bad_field[:name]}` - Expected `#{bad_field[:expected_type]}`, but found `#{bad_field[:type]}`"
|
61
|
+
end
|
62
|
+
bad_types_message = "The following fields have incorrect types#{bad_types_list.join}"
|
63
|
+
|
64
|
+
assert bad_fields.empty?, bad_types_message
|
65
|
+
|
66
|
+
patient_and_encounter_references =
|
67
|
+
context_field.select do |member|
|
68
|
+
member['reference']&.start_with?('Patient/') || member['reference']&.start_with?('Encounter/')
|
69
|
+
end
|
70
|
+
good_patient_and_encounter_roles =
|
71
|
+
patient_and_encounter_references.all? do |reference|
|
72
|
+
reference['role'].present? && reference['role'] != 'launch'
|
73
|
+
end
|
74
|
+
|
75
|
+
assert good_patient_and_encounter_roles,
|
76
|
+
'Patient and Encounter references are not allowed unless they have a role other than `launch`'
|
77
|
+
|
78
|
+
assert context_field.all? { |member| member['role'].is_a?(String) ? member['role'].present? : true },
|
79
|
+
'`role` SHALL NOT be an empty string'
|
80
|
+
|
81
|
+
required_fields = ['reference', 'canonical', 'identifier']
|
82
|
+
|
83
|
+
assert context_field.all? { |member| required_fields.any? { |field| member[field].present? } },
|
84
|
+
'Each object in fhirContext SHALL include at least one of "reference", "canonical", or "identifier"'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'smart_udap_context_test'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_IntentContextTest < SMART_UDAP_ContextTest # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_intent_context
|
6
|
+
title 'Support for "intent" launch context'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This test validates the presence and format of the "intent"
|
9
|
+
launch context.
|
10
|
+
DESCRIPTION
|
11
|
+
|
12
|
+
def context_field_name
|
13
|
+
'intent'
|
14
|
+
end
|
15
|
+
|
16
|
+
def context_scopes
|
17
|
+
[].freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_context_field
|
21
|
+
assert context_field.is_a?(String),
|
22
|
+
"Expected `#{context_field_name}` to be a String, but found: `#{context_field.class.name}`"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative 'smart_udap_encounter_context_test'
|
2
|
+
require_relative 'smart_udap_fhir_context_test'
|
3
|
+
require_relative 'smart_udap_intent_context_test'
|
4
|
+
require_relative 'smart_udap_need_patient_banner_context_test'
|
5
|
+
require_relative 'smart_udap_patient_context_test'
|
6
|
+
require_relative 'smart_udap_smart_style_url_context_test'
|
7
|
+
require_relative 'smart_udap_tenant_context_test'
|
8
|
+
require_relative 'smart_udap_openid_connect_group'
|
9
|
+
|
10
|
+
module SMART_UDAP_HarmonizationTestKit
|
11
|
+
class SMART_UDAP_LaunchContextGroup < Inferno::TestGroup # rubocop:disable Naming/ClassAndModuleCamelCase
|
12
|
+
title 'SMART/UDAP Launch Context'
|
13
|
+
id :smart_udap_launch_context
|
14
|
+
description ''
|
15
|
+
|
16
|
+
test from: :smart_udap_patient_context,
|
17
|
+
optional: true,
|
18
|
+
config: {
|
19
|
+
inputs: {
|
20
|
+
token_response_body: {
|
21
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
test from: :smart_udap_encounter_context,
|
27
|
+
optional: true,
|
28
|
+
config: {
|
29
|
+
inputs: {
|
30
|
+
token_response_body: {
|
31
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
32
|
+
}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
test from: :smart_udap_fhir_context,
|
37
|
+
optional: true,
|
38
|
+
config: {
|
39
|
+
inputs: {
|
40
|
+
token_response_body: {
|
41
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
test from: :smart_udap_need_patient_banner_context,
|
47
|
+
optional: true,
|
48
|
+
config: {
|
49
|
+
inputs: {
|
50
|
+
token_response_body: {
|
51
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
test from: :smart_udap_intent_context,
|
57
|
+
optional: true,
|
58
|
+
config: {
|
59
|
+
inputs: {
|
60
|
+
token_response_body: {
|
61
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
test from: :smart_udap_smart_style_url_context,
|
67
|
+
optional: true,
|
68
|
+
config: {
|
69
|
+
inputs: {
|
70
|
+
token_response_body: {
|
71
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
test from: :smart_udap_tenant_context,
|
77
|
+
optional: true,
|
78
|
+
config: {
|
79
|
+
inputs: {
|
80
|
+
token_response_body: {
|
81
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'smart_udap_context_test'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_NeedPatientBannerContextTest < SMART_UDAP_ContextTest # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_need_patient_banner_context
|
6
|
+
title 'Support for "need_patient_banner" launch context'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This test validates the presence and format of the "need_patient_banner"
|
9
|
+
launch context.
|
10
|
+
DESCRIPTION
|
11
|
+
|
12
|
+
def context_field_name
|
13
|
+
'need_patient_banner'
|
14
|
+
end
|
15
|
+
|
16
|
+
def context_scopes
|
17
|
+
[].freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_context_field
|
21
|
+
assert context_field == true || context_field == false,
|
22
|
+
"Expected `#{context_field_name}` to be a boolean, but found `#{context_field.class.name}`"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'smart_app_launch/openid_connect_group'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_OpenIDConnectGroup < SMARTAppLaunch::OpenIDConnectGroup # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_openid_connect
|
6
|
+
title 'Support for OpenID Connect'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This group verifies support for OpenID Connect.
|
9
|
+
DESCRIPTION
|
10
|
+
|
11
|
+
run_as_group
|
12
|
+
|
13
|
+
config(
|
14
|
+
inputs: {
|
15
|
+
client_id: {
|
16
|
+
name: :udap_client_id,
|
17
|
+
title: 'UDAP Client ID'
|
18
|
+
},
|
19
|
+
token_response_body: {
|
20
|
+
name: :udap_auth_code_flow_token_exchange_response_body
|
21
|
+
},
|
22
|
+
requested_scopes: {
|
23
|
+
name: :udap_auth_code_flow_registration_scope,
|
24
|
+
title: 'Requested Scopes',
|
25
|
+
description: 'Scopes client requested from the authorization server during the authorization step.'
|
26
|
+
},
|
27
|
+
url: {
|
28
|
+
name: :udap_fhir_base_url,
|
29
|
+
title: 'FHIR Server URL'
|
30
|
+
}
|
31
|
+
}
|
32
|
+
)
|
33
|
+
|
34
|
+
test do
|
35
|
+
id :smart_udap_openid_connect_setup
|
36
|
+
title 'OpenID Connect Test Setup'
|
37
|
+
|
38
|
+
input :token_response_body,
|
39
|
+
title: 'Token Exchange Response Body',
|
40
|
+
description: 'JSON response body returned by the authorization server during the token exchange step'
|
41
|
+
|
42
|
+
input :udap_auth_code_flow_token_retrieval_time,
|
43
|
+
title: 'Token Retrieval Time'
|
44
|
+
|
45
|
+
output :id_token,
|
46
|
+
:access_token,
|
47
|
+
:smart_credentials
|
48
|
+
|
49
|
+
run do
|
50
|
+
assert_valid_json(token_response_body)
|
51
|
+
|
52
|
+
token_response = JSON.parse(token_response_body)
|
53
|
+
|
54
|
+
output id_token: token_response['id_token'],
|
55
|
+
access_token: token_response['access_token'],
|
56
|
+
smart_credentials: {
|
57
|
+
access_token: token_response_body['access_token'],
|
58
|
+
expires_in: token_response_body['expires_in'],
|
59
|
+
udap_auth_code_flow_token_retrieval_time:
|
60
|
+
}.to_json
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
children.unshift children.pop
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative 'smart_udap_context_test'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_PatientContextTest < SMART_UDAP_ContextTest # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_patient_context
|
6
|
+
title 'Support for "patient" launch context'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This test validates the presence of the "patient" launch context field if
|
9
|
+
the `launch` or `launch/patient` scopes were granted.
|
10
|
+
|
11
|
+
If the "patient" field is present, this test will the verify that it can
|
12
|
+
retrieve that Patient resource using the granted access token.
|
13
|
+
DESCRIPTION
|
14
|
+
|
15
|
+
FHIR_ID_REGEX = /[A-Za-z0-9\-\.]{1,64}/
|
16
|
+
|
17
|
+
input :access_token,
|
18
|
+
title: 'Access Token',
|
19
|
+
description: 'Access token granted by authorization server.'
|
20
|
+
input :udap_fhir_base_url,
|
21
|
+
title: 'FHIR Server URL'
|
22
|
+
|
23
|
+
fhir_client do
|
24
|
+
url :udap_fhir_base_url
|
25
|
+
bearer_token :access_token
|
26
|
+
end
|
27
|
+
|
28
|
+
def context_field_name
|
29
|
+
'patient'
|
30
|
+
end
|
31
|
+
|
32
|
+
def context_scopes
|
33
|
+
['launch', 'launch/patient'].freeze
|
34
|
+
end
|
35
|
+
|
36
|
+
def missing_requested_context_scopes?
|
37
|
+
context_scopes.none? { |scope| requested_scopes_list.include? scope }
|
38
|
+
end
|
39
|
+
|
40
|
+
def missing_received_context_scopes?
|
41
|
+
context_scopes.none? { |scope| received_scopes_list.include? scope }
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_context_field
|
45
|
+
assert context_field.is_a?(String),
|
46
|
+
"Expected `#{context_field_name}` to be a String, but found: `#{context_field.class.name}`"
|
47
|
+
|
48
|
+
warn do
|
49
|
+
assert context_field.match?(FHIR_ID_REGEX), "`#{context_field}` is not a valid FHIR resource id."
|
50
|
+
end
|
51
|
+
|
52
|
+
fhir_read(:patient, context_field)
|
53
|
+
|
54
|
+
assert_response_status(200)
|
55
|
+
assert_resource_type(:patient)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_RequestBuilder # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
# Client MUST authenticate to auth server during token refresh per RFC 6749
|
6
|
+
# Section 6 https://datatracker.ietf.org/doc/html/rfc6749#section-6
|
7
|
+
# Assuming auth mechanism is same as it was for token exchange, i.e.,
|
8
|
+
# signed client assertion JWT
|
9
|
+
def self.build_token_refresh_request(client_assertion_jwt, refresh_token, requested_scopes)
|
10
|
+
token_refresh_headers = {
|
11
|
+
'Accept' => 'application/json',
|
12
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
13
|
+
}
|
14
|
+
|
15
|
+
token_refresh_body = {
|
16
|
+
'grant_type' => 'refresh_token',
|
17
|
+
'refresh_token' => refresh_token,
|
18
|
+
'scope' => requested_scopes,
|
19
|
+
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
20
|
+
'client_assertion' => client_assertion_jwt,
|
21
|
+
'udap' => '1'
|
22
|
+
}.compact
|
23
|
+
|
24
|
+
[token_refresh_headers, URI.encode_www_form(token_refresh_body)]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'smart_udap_context_test'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_SmartStyleUrlContextTest < SMART_UDAP_ContextTest # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_smart_style_url_context
|
6
|
+
title 'Support for "smart_style_url" launch context'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This test validates the presence and format of the "smart_style_url"
|
9
|
+
launch context. It then makes a GET request to the "smart_style_url" and
|
10
|
+
verifies that the response is valid JSON.
|
11
|
+
DESCRIPTION
|
12
|
+
|
13
|
+
def context_field_name
|
14
|
+
'smart_style_url'
|
15
|
+
end
|
16
|
+
|
17
|
+
def context_scopes
|
18
|
+
[].freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate_context_field
|
22
|
+
assert context_field.is_a?(String),
|
23
|
+
"Expected `#{context_field_name}` to be a String, but found: `#{context_field.class.name}`"
|
24
|
+
|
25
|
+
assert_valid_http_uri context_field
|
26
|
+
|
27
|
+
get(token_response['smart_style_url'])
|
28
|
+
|
29
|
+
assert_response_status(200)
|
30
|
+
assert_valid_json(response[:body])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'smart_udap_context_test'
|
2
|
+
|
3
|
+
module SMART_UDAP_HarmonizationTestKit
|
4
|
+
class SMART_UDAP_TenantContextTest < SMART_UDAP_ContextTest # rubocop:disable Naming/ClassAndModuleCamelCase
|
5
|
+
id :smart_udap_tenant_context
|
6
|
+
title 'Support for "tenant" launch context'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
This test validates the presence and format of the "tenant"
|
9
|
+
launch context.
|
10
|
+
DESCRIPTION
|
11
|
+
|
12
|
+
def context_field_name
|
13
|
+
'tenant'
|
14
|
+
end
|
15
|
+
|
16
|
+
def context_scopes
|
17
|
+
[].freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_context_field
|
21
|
+
assert context_field.is_a?(String),
|
22
|
+
"Expected `#{context_field_name}` to be a String, but found: `#{context_field.class.name}`"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'udap_security_test_kit/udap_client_assertion_payload_builder'
|
2
|
+
require 'udap_security_test_kit/udap_jwt_builder'
|
3
|
+
require_relative 'smart_udap_request_builder'
|
4
|
+
module SMART_UDAP_HarmonizationTestKit
|
5
|
+
class SMART_UDAP_TokenRefreshTest < Inferno::Test # rubocop:disable Naming/ClassAndModuleCamelCase
|
6
|
+
title 'Server successfully refreshes the access token'
|
7
|
+
id :smart_udap_token_refresh
|
8
|
+
|
9
|
+
description %(
|
10
|
+
This test will attempt to exchange the refresh token received in the original token exchange for a new access
|
11
|
+
token. The test will skip if no refresh token was granted during the token exchange test.
|
12
|
+
|
13
|
+
The [HL7 UDAP STU1.0 IG Section on Refresh Tokens](https://hl7.org/fhir/us/udap-security/STU1/consumer.html#refresh-tokens)
|
14
|
+
defers to the refresh token exchange requirements outlined in [Section 6 of RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-6),
|
15
|
+
which states:
|
16
|
+
> If the client type is confidential or
|
17
|
+
the client was issued client credentials (or assigned other
|
18
|
+
authentication requirements), the client MUST authenticate with the
|
19
|
+
authorization server as described in [Section 3.2.1](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1).
|
20
|
+
|
21
|
+
RFC 6749 section 3.2.1 references [section 2.3](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3), which
|
22
|
+
states:
|
23
|
+
> The client and authorization
|
24
|
+
server establish a client authentication method suitable for the
|
25
|
+
security requirements of the authorization server. The authorization
|
26
|
+
server MAY accept any form of client authentication meeting its
|
27
|
+
security requirements.
|
28
|
+
|
29
|
+
Therefore, Inferno will authenticate to the authorization server using the same UDAP authentication method
|
30
|
+
described in the [HL7 UDAP Consumer Facing Authentication Section 4.2](https://hl7.org/fhir/us/udap-security/STU1/consumer.html#obtaining-an-access-token),
|
31
|
+
with the following changes:
|
32
|
+
* `code` and `redirect_uri` parameters are omitted
|
33
|
+
* `grant_type` is set to `refresh_token` per [Section 6 of RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-6)
|
34
|
+
* `refresh_token` is included
|
35
|
+
)
|
36
|
+
|
37
|
+
input :udap_client_id,
|
38
|
+
title: 'Client ID'
|
39
|
+
|
40
|
+
input :udap_token_endpoint,
|
41
|
+
title: 'Token Endpoint',
|
42
|
+
description: 'The full URL from which Inferno will request to exchange a refresh token for a new access token'
|
43
|
+
|
44
|
+
input :udap_refresh_token,
|
45
|
+
title: 'Refresh Token',
|
46
|
+
type: 'textarea'
|
47
|
+
|
48
|
+
input :udap_received_scopes,
|
49
|
+
title: 'Requested Scopes',
|
50
|
+
description: 'A list of scopes that will be requested during token exchange.'
|
51
|
+
|
52
|
+
input :udap_auth_code_flow_client_cert_pem,
|
53
|
+
title: 'X.509 Client Certificate (PEM Format)',
|
54
|
+
type: 'textarea',
|
55
|
+
description: %(
|
56
|
+
A list of one or more X.509 certificates in PEM format separated by a newline.
|
57
|
+
The first (leaf) certificate MUST
|
58
|
+
represent the client entity Inferno registered as,
|
59
|
+
and the trust chain that will be built from the provided certificate(s) must resolve to a CA trusted by the
|
60
|
+
authorization server under test.
|
61
|
+
)
|
62
|
+
|
63
|
+
input :udap_auth_code_flow_client_private_key,
|
64
|
+
type: 'textarea',
|
65
|
+
title: 'Client Private Key (PEM Format)',
|
66
|
+
description: 'The private key corresponding to the X.509 client certificate'
|
67
|
+
|
68
|
+
input :udap_jwt_signing_alg,
|
69
|
+
title: 'JWT Signing Algorithm',
|
70
|
+
description: %(
|
71
|
+
Algorithm used to sign UDAP JSON Web Tokens (JWTs). UDAP Implementations SHALL support
|
72
|
+
RS256.
|
73
|
+
),
|
74
|
+
type: 'radio',
|
75
|
+
options: {
|
76
|
+
list_options: [
|
77
|
+
{
|
78
|
+
label: 'RS256',
|
79
|
+
value: 'RS256'
|
80
|
+
}
|
81
|
+
]
|
82
|
+
},
|
83
|
+
default: 'RS256',
|
84
|
+
locked: true
|
85
|
+
|
86
|
+
output :smart_udap_refresh_token_retrieval_time,
|
87
|
+
:smart_udap_token_refresh_response_body
|
88
|
+
|
89
|
+
makes_request :smart_udap_token_refresh_request
|
90
|
+
|
91
|
+
run do
|
92
|
+
client_assertion_payload = UDAPSecurityTestKit::UDAPClientAssertionPayloadBuilder.build(
|
93
|
+
udap_client_id,
|
94
|
+
udap_token_endpoint,
|
95
|
+
nil
|
96
|
+
)
|
97
|
+
|
98
|
+
x5c_certs = UDAPSecurityTestKit::UDAPJWTBuilder.split_user_input_cert_string(udap_auth_code_flow_client_cert_pem)
|
99
|
+
|
100
|
+
client_assertion_jwt = UDAPSecurityTestKit::UDAPJWTBuilder.encode_jwt_with_x5c_header(
|
101
|
+
client_assertion_payload,
|
102
|
+
udap_auth_code_flow_client_private_key,
|
103
|
+
udap_jwt_signing_alg,
|
104
|
+
x5c_certs
|
105
|
+
)
|
106
|
+
|
107
|
+
requested_scopes = (udap_received_scopes if config.options[:include_scopes])
|
108
|
+
|
109
|
+
token_refresh_headers, token_refresh_body =
|
110
|
+
SMART_UDAP_RequestBuilder.build_token_refresh_request(
|
111
|
+
client_assertion_jwt,
|
112
|
+
udap_refresh_token,
|
113
|
+
requested_scopes
|
114
|
+
)
|
115
|
+
|
116
|
+
post(udap_token_endpoint,
|
117
|
+
body: token_refresh_body,
|
118
|
+
name: :token_exchange,
|
119
|
+
headers: token_refresh_headers)
|
120
|
+
|
121
|
+
assert_response_status(200)
|
122
|
+
assert_valid_json(request.response_body)
|
123
|
+
|
124
|
+
output smart_udap_refresh_token_retrieval_time: Time.now.iso8601
|
125
|
+
|
126
|
+
output smart_udap_token_refresh_response_body: request.response_body
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative 'smart_udap_token_refresh_test'
|
2
|
+
require_relative 'smart_udap_token_response_scope_test'
|
3
|
+
require 'udap_security_test_kit/token_exchange_response_body_test'
|
4
|
+
require 'udap_security_test_kit/token_exchange_response_headers_test'
|
5
|
+
|
6
|
+
module SMART_UDAP_HarmonizationTestKit
|
7
|
+
class SMART_UDAP_TokenRefreshWithScopesGroup < Inferno::TestGroup # rubocop:disable Naming/ClassAndModuleCamelCase
|
8
|
+
title 'Support for Token Refresh With Scopes'
|
9
|
+
id :smart_udap_token_refresh_with_scopes
|
10
|
+
|
11
|
+
def self.token_refresh_group_description
|
12
|
+
%(
|
13
|
+
This group tests the ability of the system to successfully
|
14
|
+
exchange a refresh token for an access token. Refresh tokens are typically
|
15
|
+
longer lived than access tokens and allow client applications to obtain a
|
16
|
+
new access token Refresh tokens themselves cannot provide access to
|
17
|
+
resources on the server.
|
18
|
+
|
19
|
+
Per the [HL7 UDAP STU1.0 IG Section on Refresh Tokens](https://hl7.org/fhir/us/udap-security/STU1/consumer.html#refresh-tokens)
|
20
|
+
authorization server support for refresh tokens is optional:
|
21
|
+
>This guide supports the use of refresh tokens, as described in [Section 1.5 of RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-1.5).
|
22
|
+
>Authorization Servers **MAY** issue refresh tokens to consumer-facing client applications as per
|
23
|
+
>[Section 5 of RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-5).
|
24
|
+
>Client apps that have been issued refresh tokens **MAY** make refresh requests to the token endpoint as per
|
25
|
+
>[Section 6 of RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-6).
|
26
|
+
|
27
|
+
These tests will execute if the authorization server granted a refresh token during the authorization and
|
28
|
+
authentication tests. They will attempt to exchange the refresh token for a new access token via a POST request
|
29
|
+
to the token exchange endpoint and then verify the information returned as done in Section 1.3 tests 4-6.
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
scopes_included_description = %(
|
34
|
+
In the token exchange request, the optional `scope` parameter will be included. The requested scopes will default
|
35
|
+
to those granted by the authorization server in the initial token exchange request.
|
36
|
+
)
|
37
|
+
|
38
|
+
description %(
|
39
|
+
#{token_refresh_group_description}
|
40
|
+
#{scopes_included_description}
|
41
|
+
)
|
42
|
+
|
43
|
+
run_as_group
|
44
|
+
|
45
|
+
test from: :smart_udap_token_refresh,
|
46
|
+
config: {
|
47
|
+
options: { include_scopes: true }
|
48
|
+
}
|
49
|
+
|
50
|
+
test from: :udap_token_exchange_response_body,
|
51
|
+
config: {
|
52
|
+
inputs: {
|
53
|
+
token_response_body: {
|
54
|
+
name: :smart_udap_token_refresh_response_body
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
test from: :smart_udap_token_response_scope,
|
60
|
+
config: {
|
61
|
+
inputs: {
|
62
|
+
udap_auth_code_flow_token_exchange_response_body: {
|
63
|
+
name: :smart_udap_token_refresh_response_body
|
64
|
+
},
|
65
|
+
udap_auth_code_flow_registration_scope: {
|
66
|
+
name: :udap_received_scopes
|
67
|
+
},
|
68
|
+
udap_auth_code_flow_token_retrieval_time: {
|
69
|
+
name: :smart_udap_refresh_token_retrieval_time
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
test from: :udap_token_exchange_response_headers,
|
75
|
+
config: {
|
76
|
+
requests: {
|
77
|
+
name: :smart_udap_token_refresh_request
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|