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.
Files changed (23) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/smart_udap_harmonization_test_kit/smart_udap_authorization_code_authentication_group.rb +50 -0
  4. data/lib/smart_udap_harmonization_test_kit/smart_udap_authorization_code_group.rb +127 -0
  5. data/lib/smart_udap_harmonization_test_kit/smart_udap_authorization_code_redirect_test.rb +88 -0
  6. data/lib/smart_udap_harmonization_test_kit/smart_udap_context_test.rb +94 -0
  7. data/lib/smart_udap_harmonization_test_kit/smart_udap_encounter_context_test.rb +59 -0
  8. data/lib/smart_udap_harmonization_test_kit/smart_udap_fhir_context_test.rb +87 -0
  9. data/lib/smart_udap_harmonization_test_kit/smart_udap_intent_context_test.rb +25 -0
  10. data/lib/smart_udap_harmonization_test_kit/smart_udap_launch_context_group.rb +86 -0
  11. data/lib/smart_udap_harmonization_test_kit/smart_udap_need_patient_banner_context_test.rb +25 -0
  12. data/lib/smart_udap_harmonization_test_kit/smart_udap_openid_connect_group.rb +66 -0
  13. data/lib/smart_udap_harmonization_test_kit/smart_udap_patient_context_test.rb +58 -0
  14. data/lib/smart_udap_harmonization_test_kit/smart_udap_request_builder.rb +27 -0
  15. data/lib/smart_udap_harmonization_test_kit/smart_udap_smart_style_url_context_test.rb +33 -0
  16. data/lib/smart_udap_harmonization_test_kit/smart_udap_tenant_context_test.rb +25 -0
  17. data/lib/smart_udap_harmonization_test_kit/smart_udap_token_refresh_test.rb +129 -0
  18. data/lib/smart_udap_harmonization_test_kit/smart_udap_token_refresh_with_scopes_group.rb +81 -0
  19. data/lib/smart_udap_harmonization_test_kit/smart_udap_token_refresh_without_scopes_group.rb +70 -0
  20. data/lib/smart_udap_harmonization_test_kit/smart_udap_token_response_scope_test.rb +56 -0
  21. data/lib/smart_udap_harmonization_test_kit/version.rb +5 -0
  22. data/lib/smart_udap_harmonization_test_kit.rb +73 -0
  23. 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