smart_app_launch_test_kit 0.1.3 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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: []