smart_app_launch_test_kit 0.1.3 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 758773637de38a995aa7943da1291685f655e97f829c2e11d7adfc54d129ad42
4
- data.tar.gz: b0c2adfcf695418617999e414a0d85766e440e943787d811ffb54218810fee2c
3
+ metadata.gz: c2dbada5c6124d5f5ca37325fd8e5a55c9a38ce827f9129362708559d32c3612
4
+ data.tar.gz: 55f25f85ffa99273ae104690985c7fbd58bcfc65fc6939b69c96e96c0e379c75
5
5
  SHA512:
6
- metadata.gz: 9b9aabc2d05bf3b3a3fe49f35fe0be2e634f7c774d86d3d6a0129aa1cd424f1c5cebd03c84c24855445a0baf95ac23c730d614e9f628a946b5ac8d5ca0b6f8fa
7
- data.tar.gz: b287eaf62940d60d904af996c0cbbd90d59ba68ce79cdbe6400a20515b9eccb6aa0ff6c094bd07cfd86b928a11c3b6e7e740c896d125c62f5da65a857450ed38
6
+ metadata.gz: 810a79b3a167dda729558308b165b24a6ed6ed431756584708b3b9ce8107a7361fa7881f3d86ec3f03d6fbc7fbf3fb33d4ef0dc669a696f0abdb7f96e27bffa7
7
+ data.tar.gz: bb60086d05486463f7b2c9b7b17dc7204e7d7cd0e2960d1df503549a72cabe9f30cae92fe55ae6c9cd4cf39a29601e9960f1e5c5879a859c17910c1205bb479f
@@ -12,13 +12,23 @@ module SMARTAppLaunch
12
12
 
13
13
  config options: { launch_uri: "#{Inferno::Application['base_url']}/custom/smart/launch" }
14
14
 
15
+ def wait_message
16
+ return instance_exec(&config.options[:launch_message_proc]) if config.options[:launch_message_proc].present?
17
+
18
+ %(
19
+ ### #{self.class.parent&.parent&.title}
20
+
21
+ Waiting for Inferno to be launched from the EHR.
22
+
23
+ Tests will resume once Inferno receives a launch request at
24
+ `#{config.options[:launch_uri]}` with an `iss` of `#{url}`.
25
+ )
26
+ end
27
+
15
28
  run do
16
29
  wait(
17
30
  identifier: url,
18
- message: %(
19
- Waiting to receive a request at
20
- `#{config.options[:launch_uri]}` with an `iss` of `#{url}`.
21
- )
31
+ message: wait_message
22
32
  )
23
33
  end
24
34
  end
@@ -58,10 +58,17 @@ module SMARTAppLaunch
58
58
  end
59
59
 
60
60
  def wait_message(auth_url)
61
+ if config.options[:redirect_message_proc].present?
62
+ return instance_exec(auth_url, &config.options[:redirect_message_proc])
63
+ end
64
+
61
65
  %(
66
+ ### #{self.class.parent&.parent&.title}
67
+
62
68
  [Follow this link to authorize with the SMART server](#{auth_url}).
63
- Waiting to receive a request at `#{config.options[:redirect_uri]}` with
64
- a state of `#{state}`.
69
+
70
+ Tests will resume once Inferno receives a request at
71
+ `#{config.options[:redirect_uri]}` with a state of `#{state}`.
65
72
  )
66
73
  end
67
74
 
@@ -0,0 +1,45 @@
1
+ require 'uri'
2
+ require_relative 'app_redirect_test'
3
+
4
+ module SMARTAppLaunch
5
+ class AppRedirectTestSTU2 < AppRedirectTest
6
+ id :smart_app_redirect_stu2
7
+ description %(
8
+ Client browser redirected from OAuth server to redirect URI of client
9
+ app as described in SMART authorization sequence.
10
+
11
+ Client SHALL use either the HTTP GET or the HTTP POST method to send the
12
+ Authorization Request to the Authorization Server.
13
+
14
+ [Authorization Code
15
+ Request](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#request-4)
16
+ )
17
+
18
+ input :authorization_method,
19
+ title: 'Authorization Method',
20
+ type: 'radio',
21
+ default: 'get',
22
+ options: {
23
+ list_options: [
24
+ {
25
+ label: 'GET',
26
+ value: 'get'
27
+ },
28
+ {
29
+ label: 'POST',
30
+ value: 'post'
31
+ }
32
+ ]
33
+ }
34
+
35
+ def authorization_url_builder(url, params)
36
+ return super if authorization_method == 'get'
37
+
38
+ post_params = params.merge(auth_url: url)
39
+
40
+ post_url = URI(config.options[:post_authorization_uri])
41
+ post_url.query = URI.encode_www_form(post_params)
42
+ post_url.to_s
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,8 @@
1
+ require_relative 'well_known_capabilities_stu1_test'
2
+ require_relative 'well_known_endpoint_test'
3
+
1
4
  module SMARTAppLaunch
2
- class DiscoveryGroup < Inferno::TestGroup
5
+ class DiscoverySTU1Group < Inferno::TestGroup
3
6
  id :smart_discovery
4
7
  title 'SMART on FHIR Discovery'
5
8
  short_description 'Retrieve server\'s SMART on FHIR configuration.'
@@ -36,83 +39,10 @@ module SMARTAppLaunch
36
39
  * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
37
40
  )
38
41
 
39
- test do
40
- title 'FHIR server makes SMART configuration available from well-known endpoint'
41
- description %(
42
- The authorization endpoints accepted by a FHIR resource server can
43
- be exposed as a Well-Known Uniform Resource Identifier
44
- )
45
- input :url,
46
- title: 'FHIR Endpoint',
47
- description: 'URL of the FHIR endpoint used by SMART applications'
48
- output :well_known_configuration,
49
- :well_known_authorization_url,
50
- :well_known_introspection_url,
51
- :well_known_management_url,
52
- :well_known_registration_url,
53
- :well_known_revocation_url,
54
- :well_known_token_url
55
- makes_request :smart_well_known_configuration
56
-
57
- run do
58
- well_known_configuration_url = "#{url.chomp('/')}/.well-known/smart-configuration"
59
- get(well_known_configuration_url, name: :smart_well_known_configuration)
60
-
61
- assert_response_status(200)
62
-
63
- assert_valid_json(request.response_body)
64
-
65
- config = JSON.parse(request.response_body)
66
- output well_known_configuration: request.response_body,
67
- well_known_authorization_url: config['authorization_endpoint'],
68
- well_known_introspection_url: config['introspection_endpoint'],
69
- well_known_management_url: config['management_endpoint'],
70
- well_known_registration_url: config['registration_endpoint'],
71
- well_known_revocation_url: config['revocation_endpoint'],
72
- well_known_token_url: config['token_endpoint']
73
-
74
- content_type = request.response_header('Content-Type')&.value
75
-
76
- assert content_type.present?, 'No `Content-Type` header received.'
77
- assert content_type.start_with?('application/json'),
78
- "`Content-Type` must be `application/json`, but received: `#{content_type}`"
79
- end
80
- end
81
-
82
- test do
83
- title 'Well-known configuration contains required fields'
84
- description %(
85
- The JSON from .well-known/smart-configuration contains the following
86
- required fields: `authorization_endpoint`, `token_endpoint`,
87
- `capabilities`
88
- )
89
- input :well_known_configuration
90
-
91
- run do
92
- skip_if well_known_configuration.blank?, 'No well-known configuration found'
93
- config = JSON.parse(well_known_configuration)
94
-
95
- ['authorization_endpoint', 'token_endpoint', 'capabilities'].each do |key|
96
- assert config.key?(key), "Well-known configuration does not include `#{key}`"
97
- assert config[key].present?, "Well-known configuration field `#{key}` is blank"
98
- end
99
-
100
- assert config['authorization_endpoint'].is_a?(String),
101
- 'Well-known `authorization_endpoint` field must be a string'
102
- assert config['token_endpoint'].is_a?(String),
103
- 'Well-known `token_endpoint` field must be a string'
104
- assert config['capabilities'].is_a?(Array),
105
- 'Well-known `capabilities` field must be an array'
106
-
107
- non_string_capabilities = config['capabilities'].reject { |capability| capability.is_a? String }
108
-
109
- assert non_string_capabilities.blank?, %(
110
- Well-known `capabilities` field must be an array of strings, but found
111
- non-string values:
112
- #{non_string_capabilities.map { |value| "`#{value.nil? ? 'nil' : value}`" }.join(', ')}
113
- )
114
- end
115
- end
42
+ test from: :well_known_endpoint,
43
+ id: 'Test01'
44
+ test from: :well_known_capabilities_stu1,
45
+ id: 'Test02'
116
46
 
117
47
  test do
118
48
  title 'Conformance/CapabilityStatement provides OAuth 2.0 endpoints'
@@ -141,13 +71,14 @@ module SMARTAppLaunch
141
71
  resource
142
72
  .rest
143
73
  &.map(&:security)
74
+ &.compact
144
75
  &.find do |security|
145
- security.service&.any? do |service|
146
- service.coding&.any? do |coding|
147
- coding.code == 'SMART-on-FHIR'
76
+ security.service&.any? do |service|
77
+ service.coding&.any? do |coding|
78
+ coding.code == 'SMART-on-FHIR'
79
+ end
148
80
  end
149
81
  end
150
- end
151
82
  &.extension
152
83
  &.find do |extension|
153
84
  extension.url == 'http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris'
@@ -0,0 +1,50 @@
1
+ require_relative 'well_known_capabilities_stu2_test'
2
+ require_relative 'well_known_endpoint_test'
3
+
4
+ module SMARTAppLaunch
5
+ class DiscoverySTU2Group < Inferno::TestGroup
6
+ id :smart_discovery_stu2
7
+ title 'SMART on FHIR Discovery'
8
+ short_description 'Retrieve server\'s SMART on FHIR configuration.'
9
+ description %(
10
+ # Background
11
+
12
+ The #{title} Sequence test looks for authorization endpoints and SMART
13
+ capabilities as described by the [SMART App Launch
14
+ Framework](http://hl7.org/fhir/smart-app-launch/STU2/).
15
+ The SMART launch framework uses OAuth 2.0 to *authorize* apps, like
16
+ Inferno, to access certain information on a FHIR server. The
17
+ authorization service accessed at the endpoint allows users to give
18
+ these apps permission without sharing their credentials with the
19
+ application itself. Instead, the application receives an access token
20
+ which allows it to access resources on the server. The access token
21
+ itself has a limited lifetime and permission scopes associated with it.
22
+ A refresh token may also be provided to the application in order to
23
+ obtain another access token. Unlike access tokens, a refresh token is
24
+ not shared with the resource server. If OpenID Connect is used, an id
25
+ token may be provided as well. The id token can be used to
26
+ *authenticate* the user. The id token is digitally signed and allows the
27
+ identity of the user to be verified.
28
+
29
+ # Test Methodology
30
+
31
+ This test suite will examine the SMART on FHIR configuration contained
32
+ in the `/.well-known/smart-configuration` endpoint.
33
+
34
+ For more information see:
35
+
36
+ * [SMART App Launch Framework](http://hl7.org/fhir/smart-app-launch/STU2/)
37
+ * [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749)
38
+ * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
39
+ )
40
+
41
+ test from: :well_known_endpoint,
42
+ config: {
43
+ outputs: {
44
+ well_known_authorization_url: { name: :smart_authorization_url },
45
+ well_known_token_url: { name: :smart_token_url }
46
+ }
47
+ }
48
+ test from: :well_known_capabilities_stu2
49
+ end
50
+ end
@@ -0,0 +1,56 @@
1
+ require_relative 'app_redirect_test_stu2'
2
+ require_relative 'ehr_launch_group'
3
+
4
+ module SMARTAppLaunch
5
+ class EHRLaunchGroupSTU2 < EHRLaunchGroup
6
+ id :smart_ehr_launch_stu2
7
+ description %(
8
+ # Background
9
+
10
+ The [EHR
11
+ Launch](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#launch-app-ehr-launch)
12
+ is one of two ways in which an app can be launched, the other being
13
+ Standalone launch. In an EHR launch, the app is launched from an
14
+ existing EHR session or portal by a redirect to the registered launch
15
+ URL. The EHR provides the app two parameters:
16
+
17
+ * `iss` - Which contains the FHIR server url
18
+ * `launch` - An identifier needed for authorization
19
+
20
+ # Test Methodology
21
+
22
+ Inferno will wait for the EHR server redirect upon execution. When the
23
+ redirect is received Inferno will check for the presence of the `iss`
24
+ and `launch` parameters. The security of the authorization endpoint is
25
+ then checked and authorization is attempted using the provided `launch`
26
+ identifier.
27
+
28
+ For more information on the #{title} see:
29
+
30
+ * [SMART EHR Launch Sequence](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#launch-app-ehr-launch)
31
+ )
32
+
33
+ config(
34
+ inputs: {
35
+ use_pkce: {
36
+ default: 'true',
37
+ locked: true
38
+ },
39
+ pkce_code_challenge_method: {
40
+ default: 'S256',
41
+ locked: true
42
+ },
43
+ requested_scopes: {
44
+ default: 'launch openid fhirUser offline_access user/*.rs'
45
+ }
46
+ }
47
+ )
48
+
49
+ test from: :smart_app_redirect_stu2 do
50
+ input :launch
51
+ end
52
+
53
+ redirect_index = children.find_index { |child| child.id.to_s.end_with? 'app_redirect' }
54
+ children[redirect_index] = children.pop
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <!-- Use the highest supported document mode of Internet Explorer -->
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+
7
+ <meta charset="utf-8" />
8
+ <title>Inferno POST Authorization Redirect</title>
9
+ </head>
10
+ <body>
11
+ <noscript>You need to enable JavaScript to run this app.</noscript>
12
+ <form id="form" style="display:none;">
13
+ </form>
14
+ </body>
15
+ <script>
16
+ const params = Object.fromEntries(new URLSearchParams(window.location.search).entries());
17
+ const submitUrl = params.auth_url;
18
+ delete params.auth_url;
19
+ const form = document.getElementById('form');
20
+ form.method = 'POST';
21
+ form.action = submitUrl;
22
+
23
+ for (const property in params) {
24
+ let input = document.createElement('input');
25
+ input.setAttribute('name', property);
26
+
27
+ let value = params[property].replace(/\+/g, ' ');
28
+ input.setAttribute('value', decodeURIComponent(value));
29
+
30
+ form.appendChild(input);
31
+ }
32
+
33
+ form.submit();
34
+ </script>
35
+ </html>
@@ -0,0 +1,154 @@
1
+ require 'tls_test_kit'
2
+
3
+ require_relative 'version'
4
+ require_relative 'discovery_stu1_group'
5
+ require_relative 'standalone_launch_group'
6
+ require_relative 'ehr_launch_group'
7
+ require_relative 'openid_connect_group'
8
+ require_relative 'token_refresh_group'
9
+
10
+ module SMARTAppLaunch
11
+ class SMARTSTU1Suite < Inferno::TestSuite
12
+ id 'smart'
13
+ title 'SMART App Launch STU1'
14
+ version VERSION
15
+
16
+ resume_test_route :get, '/launch' do
17
+ request.query_parameters['iss']
18
+ end
19
+
20
+ resume_test_route :get, '/redirect' do
21
+ request.query_parameters['state']
22
+ end
23
+
24
+ config options: {
25
+ redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect",
26
+ launch_uri: "#{Inferno::Application['base_url']}/custom/smart/launch"
27
+ }
28
+
29
+ group do
30
+ title 'Standalone Launch'
31
+ id :smart_full_standalone_launch
32
+
33
+ run_as_group
34
+
35
+ group from: :smart_discovery
36
+ group from: :smart_standalone_launch
37
+
38
+ group from: :smart_openid_connect,
39
+ config: {
40
+ inputs: {
41
+ id_token: { name: :standalone_id_token },
42
+ client_id: { name: :standalone_client_id },
43
+ requested_scopes: { name: :standalone_requested_scopes },
44
+ access_token: { name: :standalone_access_token },
45
+ smart_credentials: { name: :standalone_smart_credentials }
46
+ }
47
+ }
48
+
49
+ group from: :smart_token_refresh,
50
+ id: :smart_standalone_refresh_without_scopes,
51
+ title: 'SMART Token Refresh Without Scopes',
52
+ config: {
53
+ inputs: {
54
+ refresh_token: { name: :standalone_refresh_token },
55
+ client_id: { name: :standalone_client_id },
56
+ client_secret: { name: :standalone_client_secret },
57
+ received_scopes: { name: :standalone_received_scopes }
58
+ },
59
+ outputs: {
60
+ refresh_token: { name: :standalone_refresh_token },
61
+ received_scopes: { name: :standalone_received_scopes },
62
+ access_token: { name: :standalone_access_token },
63
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
64
+ expires_in: { name: :standalone_expires_in },
65
+ smart_credentials: { name: :standalone_smart_credentials }
66
+ }
67
+ }
68
+
69
+ group from: :smart_token_refresh,
70
+ id: :smart_standalone_refresh_with_scopes,
71
+ title: 'SMART Token Refresh With Scopes',
72
+ config: {
73
+ options: { include_scopes: true },
74
+ inputs: {
75
+ refresh_token: { name: :standalone_refresh_token },
76
+ client_id: { name: :standalone_client_id },
77
+ client_secret: { name: :standalone_client_secret },
78
+ received_scopes: { name: :standalone_received_scopes }
79
+ },
80
+ outputs: {
81
+ refresh_token: { name: :standalone_refresh_token },
82
+ received_scopes: { name: :standalone_received_scopes },
83
+ access_token: { name: :standalone_access_token },
84
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
85
+ expires_in: { name: :standalone_expires_in },
86
+ smart_credentials: { name: :standalone_smart_credentials }
87
+ }
88
+ }
89
+ end
90
+
91
+ group do
92
+ title 'EHR Launch'
93
+ id :smart_full_ehr_launch
94
+
95
+ run_as_group
96
+
97
+ group from: :smart_discovery
98
+
99
+ group from: :smart_ehr_launch
100
+
101
+ group from: :smart_openid_connect,
102
+ config: {
103
+ inputs: {
104
+ id_token: { name: :ehr_id_token },
105
+ client_id: { name: :ehr_client_id },
106
+ requested_scopes: { name: :ehr_requested_scopes },
107
+ access_token: { name: :ehr_access_token },
108
+ smart_credentials: { name: :ehr_smart_credentials }
109
+ }
110
+ }
111
+
112
+ group from: :smart_token_refresh,
113
+ id: :smart_ehr_refresh_without_scopes,
114
+ title: 'SMART Token Refresh Without Scopes',
115
+ config: {
116
+ inputs: {
117
+ refresh_token: { name: :ehr_refresh_token },
118
+ client_id: { name: :ehr_client_id },
119
+ client_secret: { name: :ehr_client_secret },
120
+ received_scopes: { name: :ehr_received_scopes }
121
+ },
122
+ outputs: {
123
+ refresh_token: { name: :ehr_refresh_token },
124
+ received_scopes: { name: :ehr_received_scopes },
125
+ access_token: { name: :ehr_access_token },
126
+ token_retrieval_time: { name: :ehr_token_retrieval_time },
127
+ expires_in: { name: :ehr_expires_in },
128
+ smart_credentials: { name: :ehr_smart_credentials }
129
+ }
130
+ }
131
+
132
+ group from: :smart_token_refresh,
133
+ id: :smart_ehr_refresh_with_scopes,
134
+ title: 'SMART Token Refresh With Scopes',
135
+ config: {
136
+ options: { include_scopes: true },
137
+ inputs: {
138
+ refresh_token: { name: :ehr_refresh_token },
139
+ client_id: { name: :ehr_client_id },
140
+ client_secret: { name: :ehr_client_secret },
141
+ received_scopes: { name: :ehr_received_scopes }
142
+ },
143
+ outputs: {
144
+ refresh_token: { name: :ehr_refresh_token },
145
+ received_scopes: { name: :ehr_received_scopes },
146
+ access_token: { name: :ehr_access_token },
147
+ token_retrieval_time: { name: :ehr_token_retrieval_time },
148
+ expires_in: { name: :ehr_expires_in },
149
+ smart_credentials: { name: :ehr_smart_credentials }
150
+ }
151
+ }
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,160 @@
1
+ require 'tls_test_kit'
2
+
3
+ require_relative 'version'
4
+ require_relative 'discovery_stu2_group'
5
+ require_relative 'standalone_launch_group_stu2'
6
+ require_relative 'ehr_launch_group_stu2'
7
+ require_relative 'openid_connect_group'
8
+ require_relative 'token_refresh_group'
9
+
10
+ module SMARTAppLaunch
11
+ class SMARTSTU2Suite < Inferno::TestSuite
12
+ id 'smart_stu2'
13
+ title 'SMART App Launch STU2 (Work in Progress)'
14
+ version VERSION
15
+
16
+ resume_test_route :get, '/launch' do
17
+ request.query_parameters['iss']
18
+ end
19
+
20
+ resume_test_route :get, '/redirect' do
21
+ request.query_parameters['state']
22
+ end
23
+
24
+ @post_auth_page = File.read(File.join(__dir__, 'post_auth.html'))
25
+ post_auth_handler = proc { [200, {}, [@post_auth_page]] }
26
+
27
+ route :get, '/post_auth', post_auth_handler
28
+
29
+ config options: {
30
+ redirect_uri: "#{Inferno::Application['base_url']}/custom/smart_stu2/redirect",
31
+ launch_uri: "#{Inferno::Application['base_url']}/custom/smart_stu2/launch",
32
+ post_authorization_uri: "#{Inferno::Application['base_url']}/custom/smart_stu2/post_auth"
33
+ }
34
+
35
+ group do
36
+ title 'Standalone Launch'
37
+ id :smart_full_standalone_launch
38
+
39
+ run_as_group
40
+
41
+ group from: :smart_discovery_stu2
42
+ group from: :smart_standalone_launch_stu2
43
+
44
+ group from: :smart_openid_connect,
45
+ config: {
46
+ inputs: {
47
+ id_token: { name: :standalone_id_token },
48
+ client_id: { name: :standalone_client_id },
49
+ requested_scopes: { name: :standalone_requested_scopes },
50
+ access_token: { name: :standalone_access_token },
51
+ smart_credentials: { name: :standalone_smart_credentials }
52
+ }
53
+ }
54
+
55
+ group from: :smart_token_refresh,
56
+ id: :smart_standalone_refresh_without_scopes,
57
+ title: 'SMART Token Refresh Without Scopes',
58
+ config: {
59
+ inputs: {
60
+ refresh_token: { name: :standalone_refresh_token },
61
+ client_id: { name: :standalone_client_id },
62
+ client_secret: { name: :standalone_client_secret },
63
+ received_scopes: { name: :standalone_received_scopes }
64
+ },
65
+ outputs: {
66
+ refresh_token: { name: :standalone_refresh_token },
67
+ received_scopes: { name: :standalone_received_scopes },
68
+ access_token: { name: :standalone_access_token },
69
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
70
+ expires_in: { name: :standalone_expires_in },
71
+ smart_credentials: { name: :standalone_smart_credentials }
72
+ }
73
+ }
74
+
75
+ group from: :smart_token_refresh,
76
+ id: :smart_standalone_refresh_with_scopes,
77
+ title: 'SMART Token Refresh With Scopes',
78
+ config: {
79
+ options: { include_scopes: true },
80
+ inputs: {
81
+ refresh_token: { name: :standalone_refresh_token },
82
+ client_id: { name: :standalone_client_id },
83
+ client_secret: { name: :standalone_client_secret },
84
+ received_scopes: { name: :standalone_received_scopes }
85
+ },
86
+ outputs: {
87
+ refresh_token: { name: :standalone_refresh_token },
88
+ received_scopes: { name: :standalone_received_scopes },
89
+ access_token: { name: :standalone_access_token },
90
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
91
+ expires_in: { name: :standalone_expires_in },
92
+ smart_credentials: { name: :standalone_smart_credentials }
93
+ }
94
+ }
95
+ end
96
+
97
+ group do
98
+ title 'EHR Launch'
99
+ id :smart_full_ehr_launch
100
+
101
+ run_as_group
102
+
103
+ group from: :smart_discovery_stu2
104
+
105
+ group from: :smart_ehr_launch_stu2
106
+
107
+ group from: :smart_openid_connect,
108
+ config: {
109
+ inputs: {
110
+ id_token: { name: :ehr_id_token },
111
+ client_id: { name: :ehr_client_id },
112
+ requested_scopes: { name: :ehr_requested_scopes },
113
+ access_token: { name: :ehr_access_token },
114
+ smart_credentials: { name: :ehr_smart_credentials }
115
+ }
116
+ }
117
+
118
+ group from: :smart_token_refresh,
119
+ id: :smart_ehr_refresh_without_scopes,
120
+ title: 'SMART Token Refresh Without Scopes',
121
+ config: {
122
+ inputs: {
123
+ refresh_token: { name: :ehr_refresh_token },
124
+ client_id: { name: :ehr_client_id },
125
+ client_secret: { name: :ehr_client_secret },
126
+ received_scopes: { name: :ehr_received_scopes }
127
+ },
128
+ outputs: {
129
+ refresh_token: { name: :ehr_refresh_token },
130
+ received_scopes: { name: :ehr_received_scopes },
131
+ access_token: { name: :ehr_access_token },
132
+ token_retrieval_time: { name: :ehr_token_retrieval_time },
133
+ expires_in: { name: :ehr_expires_in },
134
+ smart_credentials: { name: :ehr_smart_credentials }
135
+ }
136
+ }
137
+
138
+ group from: :smart_token_refresh,
139
+ id: :smart_ehr_refresh_with_scopes,
140
+ title: 'SMART Token Refresh With Scopes',
141
+ config: {
142
+ options: { include_scopes: true },
143
+ inputs: {
144
+ refresh_token: { name: :ehr_refresh_token },
145
+ client_id: { name: :ehr_client_id },
146
+ client_secret: { name: :ehr_client_secret },
147
+ received_scopes: { name: :ehr_received_scopes }
148
+ },
149
+ outputs: {
150
+ refresh_token: { name: :ehr_refresh_token },
151
+ received_scopes: { name: :ehr_received_scopes },
152
+ access_token: { name: :ehr_access_token },
153
+ token_retrieval_time: { name: :ehr_token_retrieval_time },
154
+ expires_in: { name: :ehr_expires_in },
155
+ smart_credentials: { name: :ehr_smart_credentials }
156
+ }
157
+ }
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,52 @@
1
+ require_relative 'app_redirect_test_stu2'
2
+ require_relative 'standalone_launch_group'
3
+
4
+ module SMARTAppLaunch
5
+ class StandaloneLaunchGroupSTU2 < StandaloneLaunchGroup
6
+ id :smart_standalone_launch_stu2
7
+ description %(
8
+ # Background
9
+
10
+ The [Standalone
11
+ Launch Sequence](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#launch-app-standalone-launch)
12
+ allows an app, like Inferno, to be launched independent of an
13
+ existing EHR session. It is one of the two launch methods described in
14
+ the SMART App Launch Framework alongside EHR Launch. The app will
15
+ request authorization for the provided scope from the authorization
16
+ endpoint, ultimately receiving an authorization token which can be used
17
+ to gain access to resources on the FHIR server.
18
+
19
+ # Test Methodology
20
+
21
+ Inferno will redirect the user to the the authorization endpoint so that
22
+ they may provide any required credentials and authorize the application.
23
+ Upon successful authorization, Inferno will exchange the authorization
24
+ code provided for an access token.
25
+
26
+ For more information on the #{title}:
27
+
28
+ * [Standalone Launch Sequence](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#launch-app-standalone-launch)
29
+ )
30
+
31
+ config(
32
+ inputs: {
33
+ use_pkce: {
34
+ default: 'true',
35
+ locked: true
36
+ },
37
+ pkce_code_challenge_method: {
38
+ default: 'S256',
39
+ locked: true
40
+ },
41
+ requested_scopes: {
42
+ default: 'launch/patient openid fhirUser offline_access patient/*.rs'
43
+ }
44
+ }
45
+ )
46
+
47
+ test from: :smart_app_redirect_stu2
48
+
49
+ redirect_index = children.find_index { |child| child.id.to_s.end_with? 'app_redirect' }
50
+ children[redirect_index] = children.pop
51
+ end
52
+ end
@@ -16,7 +16,7 @@ module SMARTAppLaunch
16
16
  the Pragma response header field with a value of no-cache to be
17
17
  consistent with the requirements of the inital access token exchange.
18
18
  )
19
- input :well_known_token_url, :refresh_token, :client_id, :received_scopes
19
+ input :smart_token_url, :refresh_token, :client_id, :received_scopes
20
20
  input :client_secret, optional: true
21
21
  output :smart_credentials, :token_retrieval_time
22
22
  makes_request :token_refresh
@@ -39,7 +39,7 @@ module SMARTAppLaunch
39
39
  oauth2_params['client_id'] = client_id
40
40
  end
41
41
 
42
- post(well_known_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
42
+ post(smart_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
43
43
 
44
44
  assert_response_status(200)
45
45
  assert_valid_json(request.response_body)
@@ -54,7 +54,7 @@ module SMARTAppLaunch
54
54
  client_id: client_id,
55
55
  client_secret: client_secret,
56
56
  token_retrieval_time: token_retrieval_time,
57
- token_url: well_known_token_url
57
+ token_url: smart_token_url
58
58
  }.to_json
59
59
  end
60
60
  end
@@ -43,7 +43,7 @@ module SMARTAppLaunch
43
43
  validate_required_fields_present(token_response_body, ['access_token', 'token_type', 'expires_in', 'scope'])
44
44
  validate_token_field_types(token_response_body)
45
45
  validate_token_type(token_response_body)
46
- check_for_missing_scopes(requested_scopes, token_response_body)
46
+ check_for_missing_scopes(requested_scopes, token_response_body) unless config.options[:ignore_missing_scopes_check]
47
47
 
48
48
  assert access_token.present?, 'Token response did not contain an access token'
49
49
  assert token_response_body['token_type']&.casecmp('Bearer')&.zero?,
@@ -1,3 +1,3 @@
1
1
  module SMARTAppLaunch
2
- VERSION = '0.1.3'
2
+ VERSION = '0.1.6'
3
3
  end
@@ -0,0 +1,39 @@
1
+ module SMARTAppLaunch
2
+ class WellKnownCapabilitiesSTU1Test < Inferno::Test
3
+ title 'Well-known configuration contains required fields'
4
+ id :well_known_capabilities_stu1
5
+ input :well_known_configuration
6
+ description %(
7
+ The JSON from .well-known/smart-configuration contains the following
8
+ required fields: `authorization_endpoint`, `token_endpoint`,
9
+ `capabilities`
10
+ )
11
+
12
+ def required_capabilities
13
+ {
14
+ 'authorization_endpoint' => String,
15
+ 'token_endpoint' => String,
16
+ 'capabilities' => Array
17
+ }
18
+ end
19
+
20
+ run do
21
+ skip_if well_known_configuration.blank?, 'No well-known configuration found'
22
+ config = JSON.parse(well_known_configuration)
23
+
24
+ required_capabilities.each do |key, type|
25
+ assert config.key?(key), "Well-known configuration does not include `#{key}`"
26
+ assert config[key].present?, "Well-known configuration field `#{key}` is blank"
27
+ assert config[key].is_a?(type), "Well-known `#{key}` must be type: #{type.to_s.downcase}"
28
+ end
29
+
30
+ non_string_capabilities = config['capabilities'].reject { |capability| capability.is_a? String }
31
+
32
+ assert non_string_capabilities.blank?, %(
33
+ Well-known `capabilities` field must be an array of strings, but found
34
+ non-string values:
35
+ #{non_string_capabilities.map { |value| "`#{value.nil? ? 'nil' : value}`" }.join(', ')}
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,61 @@
1
+ module SMARTAppLaunch
2
+ class WellKnownCapabilitiesSTU2Test < Inferno::Test
3
+ title 'Well-known configuration contains required fields'
4
+ id :well_known_capabilities_stu2
5
+ input :well_known_configuration
6
+ description %(
7
+ The JSON from .well-known/smart-configuration contains the following
8
+ required fields: `authorization_endpoint`, `token_endpoint`,
9
+ `capabilities`, `grant_types_supported`, `code_challenge_methods_supported`.
10
+ If the `sso-openid-connect` capability is supported, then `issuer` and `jwks_uri` must be
11
+ present. If `sso-openid-connect` capability is not supported, then `issuer` must be omitted.
12
+ )
13
+
14
+ def required_capabilities
15
+ {
16
+ 'authorization_endpoint' => String,
17
+ 'token_endpoint' => String,
18
+ 'capabilities' => Array,
19
+ 'grant_types_supported' => Array,
20
+ 'code_challenge_methods_supported' => Array
21
+ }
22
+ end
23
+
24
+ run do
25
+ skip_if well_known_configuration.blank?, 'No well-known configuration found'
26
+ config = JSON.parse(well_known_configuration)
27
+
28
+ required_capabilities.each do |key, type|
29
+ assert config.key?(key), "Well-known configuration does not include `#{key}`"
30
+ assert config[key].present?, "Well-known configuration field `#{key}` is blank"
31
+ assert config[key].is_a?(type), "Well-known `#{key}` must be type: #{type.to_s.downcase}"
32
+ end
33
+
34
+ assert config['grant_types_supported'].include?('authorization_code'),
35
+ 'Well-known `grant_types_supported` must include `authorization_code` grant type to indicate SMART App Launch Support'
36
+ assert config['code_challenge_methods_supported'].include?('S256'),
37
+ 'Well-known `code_challenge_methods_supported` must include `S256`'
38
+ assert config['code_challenge_methods_supported'].exclude?('plain'),
39
+ 'Well-known `code_challenge_methods_support` must not include `plain`'
40
+
41
+ if config['capabilities'].include?('sso-openid-connect')
42
+ assert config['issuer'].is_a?(String),
43
+ 'Well-known `issuer` field must be a string and present when server capabilities includes `sso-openid-connect`'
44
+ assert config['jwks_uri'].is_a?(String),
45
+ 'Well-known `jwks_uri` field must be a string and present when server capabilites includes `sso-openid-coneect`'
46
+ else
47
+ warning do
48
+ assert config['issuer'].nil?, 'Well-known `issuer` is omitted when server capabilites does not include `sso-openid-connect`'
49
+ end
50
+ end
51
+
52
+ non_string_capabilities = config['capabilities'].reject { |capability| capability.is_a? String }
53
+
54
+ assert non_string_capabilities.blank?, %(
55
+ Well-known `capabilities` field must be an array of strings, but found
56
+ non-string values:
57
+ #{non_string_capabilities.map { |value| "`#{value.nil? ? 'nil' : value}`" }.join(', ')}
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,46 @@
1
+ module SMARTAppLaunch
2
+ class WellKnownEndpointTest < Inferno::Test
3
+ title 'FHIR server makes SMART configuration available from well-known endpoint'
4
+ id :well_known_endpoint
5
+ description %(
6
+ The authorization endpoints accepted by a FHIR resource server can
7
+ be exposed as a Well-Known Uniform Resource Identifier
8
+ )
9
+ input :url,
10
+ title: 'FHIR Endpoint',
11
+ description: 'URL of the FHIR endpoint used by SMART applications'
12
+ output :well_known_configuration,
13
+ :well_known_authorization_url,
14
+ :well_known_introspection_url,
15
+ :well_known_management_url,
16
+ :well_known_registration_url,
17
+ :well_known_revocation_url,
18
+ :well_known_token_url
19
+ makes_request :smart_well_known_configuration
20
+
21
+ run do
22
+ well_known_configuration_url = "#{url.chomp('/')}/.well-known/smart-configuration"
23
+ get(well_known_configuration_url,
24
+ name: :smart_well_known_configuration,
25
+ headers: { 'Accept' => 'application/json' })
26
+ assert_response_status(200)
27
+
28
+ assert_valid_json(request.response_body)
29
+
30
+ config = JSON.parse(request.response_body)
31
+ output well_known_configuration: request.response_body,
32
+ well_known_authorization_url: config['authorization_endpoint'],
33
+ well_known_introspection_url: config['introspection_endpoint'],
34
+ well_known_management_url: config['management_endpoint'],
35
+ well_known_registration_url: config['registration_endpoint'],
36
+ well_known_revocation_url: config['revocation_endpoint'],
37
+ well_known_token_url: config['token_endpoint']
38
+
39
+ content_type = request.response_header('Content-Type')&.value
40
+
41
+ assert content_type.present?, 'No `Content-Type` header received.'
42
+ assert content_type.start_with?('application/json'),
43
+ "`Content-Type` must be `application/json`, but received: `#{content_type}`"
44
+ end
45
+ end
46
+ end
@@ -1,174 +1,4 @@
1
1
  require 'tls_test_kit'
2
2
 
3
- require_relative 'smart_app_launch/version'
4
- require_relative 'smart_app_launch/discovery_group'
5
- require_relative 'smart_app_launch/standalone_launch_group'
6
- require_relative 'smart_app_launch/ehr_launch_group'
7
- require_relative 'smart_app_launch/openid_connect_group'
8
- require_relative 'smart_app_launch/token_refresh_group'
9
-
10
- # TODO: Remove once this functionality is released in core:
11
- # https://github.com/inferno-framework/inferno-core/pull/86
12
- module Inferno
13
- module DSL
14
- module Runnable
15
- def required_inputs(prior_outputs = [])
16
- required_inputs =
17
- inputs
18
- .reject { |input| input_definitions[input][:optional] }
19
- .map { |input| config.input_name(input) }
20
- .reject { |input| prior_outputs.include?(input) }
21
- children_required_inputs = children.flat_map { |child| child.required_inputs(prior_outputs) }
22
- prior_outputs.concat(outputs.map { |output| config.output_name(output) })
23
- (required_inputs + children_required_inputs).flatten.uniq
24
- end
25
- end
26
- end
27
- end
28
-
29
- module SMARTAppLaunch
30
- class SMARTSuite < Inferno::TestSuite
31
- id 'smart'
32
- title 'SMART App Launch STU1'
33
- version VERSION
34
-
35
- resume_test_route :get, '/launch' do
36
- request.query_parameters['iss']
37
- end
38
-
39
- resume_test_route :get, '/redirect' do
40
- request.query_parameters['state']
41
- end
42
-
43
- config options: {
44
- redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect",
45
- launch_uri: "#{Inferno::Application['base_url']}/custom/smart/launch"
46
- }
47
-
48
- group do
49
- title 'Standalone Launch'
50
- id :smart_full_standalone_launch
51
-
52
- run_as_group
53
-
54
- group from: :smart_discovery
55
-
56
- group from: :smart_standalone_launch
57
-
58
- group from: :smart_openid_connect,
59
- config: {
60
- inputs: {
61
- id_token: { name: :standalone_id_token },
62
- client_id: { name: :standalone_client_id },
63
- requested_scopes: { name: :standalone_requested_scopes },
64
- access_token: { name: :standalone_access_token },
65
- smart_credentials: { name: :standalone_smart_credentials }
66
- }
67
- }
68
-
69
- group from: :smart_token_refresh,
70
- id: :smart_standalone_refresh_without_scopes,
71
- title: 'SMART Token Refresh Without Scopes',
72
- config: {
73
- inputs: {
74
- refresh_token: { name: :standalone_refresh_token },
75
- client_id: { name: :standalone_client_id },
76
- client_secret: { name: :standalone_client_secret },
77
- received_scopes: { name: :standalone_received_scopes }
78
- },
79
- outputs: {
80
- refresh_token: { name: :standalone_refresh_token },
81
- received_scopes: { name: :standalone_received_scopes },
82
- access_token: { name: :standalone_access_token },
83
- token_retrieval_time: { name: :standalone_token_retrieval_time },
84
- expires_in: { name: :standalone_expires_in },
85
- smart_credentials: { name: :standalone_smart_credentials }
86
- }
87
- }
88
-
89
- group from: :smart_token_refresh,
90
- id: :smart_standalone_refresh_with_scopes,
91
- title: 'SMART Token Refresh With Scopes',
92
- config: {
93
- options: { include_scopes: true },
94
- inputs: {
95
- refresh_token: { name: :standalone_refresh_token },
96
- client_id: { name: :standalone_client_id },
97
- client_secret: { name: :standalone_client_secret },
98
- received_scopes: { name: :standalone_received_scopes }
99
- },
100
- outputs: {
101
- refresh_token: { name: :standalone_refresh_token },
102
- received_scopes: { name: :standalone_received_scopes },
103
- access_token: { name: :standalone_access_token },
104
- token_retrieval_time: { name: :standalone_token_retrieval_time },
105
- expires_in: { name: :standalone_expires_in },
106
- smart_credentials: { name: :standalone_smart_credentials }
107
- }
108
- }
109
- end
110
-
111
- group do
112
- title 'EHR Launch'
113
- id :smart_full_ehr_launch
114
-
115
- run_as_group
116
-
117
- group from: :smart_discovery
118
-
119
- group from: :smart_ehr_launch
120
-
121
- group from: :smart_openid_connect,
122
- config: {
123
- inputs: {
124
- id_token: { name: :ehr_id_token },
125
- client_id: { name: :ehr_client_id },
126
- requested_scopes: { name: :ehr_requested_scopes },
127
- access_token: { name: :ehr_access_token },
128
- smart_credentials: { name: :ehr_smart_credentials }
129
- }
130
- }
131
-
132
- group from: :smart_token_refresh,
133
- id: :smart_ehr_refresh_without_scopes,
134
- title: 'SMART Token Refresh Without Scopes',
135
- config: {
136
- inputs: {
137
- refresh_token: { name: :ehr_refresh_token },
138
- client_id: { name: :ehr_client_id },
139
- client_secret: { name: :ehr_client_secret },
140
- received_scopes: { name: :ehr_received_scopes }
141
- },
142
- outputs: {
143
- refresh_token: { name: :ehr_refresh_token },
144
- received_scopes: { name: :ehr_received_scopes },
145
- access_token: { name: :ehr_access_token },
146
- token_retrieval_time: { name: :ehr_token_retrieval_time },
147
- expires_in: { name: :ehr_expires_in },
148
- smart_credentials: { name: :ehr_smart_credentials }
149
- }
150
- }
151
-
152
- group from: :smart_token_refresh,
153
- id: :smart_ehr_refresh_with_scopes,
154
- title: 'SMART Token Refresh With Scopes',
155
- config: {
156
- options: { include_scopes: true },
157
- inputs: {
158
- refresh_token: { name: :ehr_refresh_token },
159
- client_id: { name: :ehr_client_id },
160
- client_secret: { name: :ehr_client_secret },
161
- received_scopes: { name: :ehr_received_scopes }
162
- },
163
- outputs: {
164
- refresh_token: { name: :ehr_refresh_token },
165
- received_scopes: { name: :ehr_received_scopes },
166
- access_token: { name: :ehr_access_token },
167
- token_retrieval_time: { name: :ehr_token_retrieval_time },
168
- expires_in: { name: :ehr_expires_in },
169
- smart_credentials: { name: :ehr_smart_credentials }
170
- }
171
- }
172
- end
173
- end
174
- end
3
+ require_relative 'smart_app_launch/smart_stu1_suite'
4
+ require_relative 'smart_app_launch/smart_stu2_suite'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_app_launch_test_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen MacVicar
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-09 00:00:00.000000000 Z
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inferno_core
@@ -133,9 +133,12 @@ files:
133
133
  - LICENSE
134
134
  - lib/smart_app_launch/app_launch_test.rb
135
135
  - lib/smart_app_launch/app_redirect_test.rb
136
+ - lib/smart_app_launch/app_redirect_test_stu2.rb
136
137
  - lib/smart_app_launch/code_received_test.rb
137
- - lib/smart_app_launch/discovery_group.rb
138
+ - lib/smart_app_launch/discovery_stu1_group.rb
139
+ - lib/smart_app_launch/discovery_stu2_group.rb
138
140
  - lib/smart_app_launch/ehr_launch_group.rb
141
+ - lib/smart_app_launch/ehr_launch_group_stu2.rb
139
142
  - lib/smart_app_launch/launch_received_test.rb
140
143
  - lib/smart_app_launch/openid_connect_group.rb
141
144
  - lib/smart_app_launch/openid_decode_id_token_test.rb
@@ -145,7 +148,11 @@ files:
145
148
  - lib/smart_app_launch/openid_retrieve_jwks_test.rb
146
149
  - lib/smart_app_launch/openid_token_header_test.rb
147
150
  - lib/smart_app_launch/openid_token_payload_test.rb
151
+ - lib/smart_app_launch/post_auth.html
152
+ - lib/smart_app_launch/smart_stu1_suite.rb
153
+ - lib/smart_app_launch/smart_stu2_suite.rb
148
154
  - lib/smart_app_launch/standalone_launch_group.rb
155
+ - lib/smart_app_launch/standalone_launch_group_stu2.rb
149
156
  - lib/smart_app_launch/token_exchange_test.rb
150
157
  - lib/smart_app_launch/token_payload_validation.rb
151
158
  - lib/smart_app_launch/token_refresh_body_test.rb
@@ -154,6 +161,9 @@ files:
154
161
  - lib/smart_app_launch/token_response_body_test.rb
155
162
  - lib/smart_app_launch/token_response_headers_test.rb
156
163
  - lib/smart_app_launch/version.rb
164
+ - lib/smart_app_launch/well_known_capabilities_stu1_test.rb
165
+ - lib/smart_app_launch/well_known_capabilities_stu2_test.rb
166
+ - lib/smart_app_launch/well_known_endpoint_test.rb
157
167
  - lib/smart_app_launch_test_kit.rb
158
168
  homepage: https://github.com/inferno_framework/smart-app-launch-test-kit
159
169
  licenses:
@@ -161,7 +171,7 @@ licenses:
161
171
  metadata:
162
172
  homepage_uri: https://github.com/inferno_framework/smart-app-launch-test-kit
163
173
  source_code_uri: https://github.com/inferno_framework/smart-app-launch-test-kit
164
- post_install_message:
174
+ post_install_message:
165
175
  rdoc_options: []
166
176
  require_paths:
167
177
  - lib
@@ -177,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
187
  version: '0'
178
188
  requirements: []
179
189
  rubygems_version: 3.1.6
180
- signing_key:
190
+ signing_key:
181
191
  specification_version: 4
182
192
  summary: Inferno Tests for the SMART Application Launch Framework Implementation Guide
183
193
  test_files: []