smart_app_launch_test_kit 0.0.1

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.
@@ -0,0 +1,45 @@
1
+ require_relative 'token_refresh_test'
2
+ require_relative 'token_refresh_body_test'
3
+ require_relative 'token_response_headers_test'
4
+
5
+ module SMARTAppLaunch
6
+ class TokenRefreshGroup < Inferno::TestGroup
7
+ id :smart_token_refresh
8
+ title 'SMART Token Refresh'
9
+ description %(
10
+ # Background
11
+
12
+ The #{title} Sequence tests the ability of the system to successfuly
13
+ exchange a refresh token for an access token. Refresh tokens are typically
14
+ longer lived than access tokens and allow client applications to obtain a
15
+ new access token Refresh tokens themselves cannot provide access to
16
+ resources on the server.
17
+
18
+ Token refreshes are accomplished through a `POST` request to the token
19
+ exchange endpoint as described in the [SMART App Launch
20
+ Framework](http://www.hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token).
21
+
22
+ # Test Methodology
23
+
24
+ This test attempts to exchange the refresh token for a new access token
25
+ and verify that the information returned contains the required fields and
26
+ uses the proper headers.
27
+
28
+ For more information see:
29
+
30
+ * [The OAuth 2.0 Authorization
31
+ Framework](https://tools.ietf.org/html/rfc6749)
32
+ * [Using a refresh token to obtain a new access
33
+ token](http://hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token)
34
+ )
35
+
36
+ test from: :smart_token_refresh
37
+ test from: :smart_token_refresh_body
38
+ test from: :smart_token_response_headers,
39
+ config: {
40
+ requests: {
41
+ token: { name: :token_refresh }
42
+ }
43
+ }
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ require_relative 'token_payload_validation'
2
+
3
+ module SMARTAppLaunch
4
+ class TokenRefreshTest < Inferno::Test
5
+ include TokenPayloadValidation
6
+
7
+ id :smart_token_refresh
8
+ title 'Server successfully refreshes the access token when optional scope parameter omitted'
9
+ description %(
10
+ Server successfully exchanges refresh token at OAuth token endpoint
11
+ without providing scope in the body of the request.
12
+
13
+ The EHR authorization server SHALL return a JSON structure that includes
14
+ an access token or a message indicating that the authorization request
15
+ has been denied. `access_token`, `expires_in`, `token_type`, and `scope` are
16
+ required. `access_token` must be `Bearer`.
17
+
18
+ Although not required in the token refresh portion of the SMART App
19
+ Launch Guide, the token refresh response should include the HTTP
20
+ Cache-Control response header field with a value of no-store, as well as
21
+ the Pragma response header field with a value of no-cache to be
22
+ consistent with the requirements of the inital access token exchange.
23
+ )
24
+ input :well_known_token_url, :refresh_token, :client_id, :received_scopes
25
+ input :client_secret, optional: true
26
+ makes_request :token_refresh
27
+
28
+ run do
29
+ skip_if refresh_token.blank?
30
+
31
+ oauth2_params = {
32
+ 'grant_type' => 'refresh_token',
33
+ 'refresh_token' => refresh_token
34
+ }
35
+ oauth2_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
36
+
37
+ oauth2_params['scope'] = received_scopes if config.options[:include_scopes]
38
+
39
+ if client_secret.present?
40
+ credentials = Base64.strict_encode64("#{client_id}:#{client_secret}")
41
+ oauth2_headers['Authorization'] = "Basic #{credentials}"
42
+ else
43
+ oauth2_params['client_id'] = client_id
44
+ end
45
+
46
+ post(well_known_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
47
+
48
+ assert_response_status(200)
49
+ assert_valid_json(response[:body])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ require_relative 'token_payload_validation'
2
+
3
+ module SMARTAppLaunch
4
+ class TokenResponseBodyTest < Inferno::Test
5
+ include TokenPayloadValidation
6
+
7
+ title 'Token exchange response body contains required information encoded in JSON'
8
+ description %(
9
+ The EHR authorization server shall return a JSON structure that includes
10
+ an access token or a message indicating that the authorization request
11
+ has been denied. `access_token`, `token_type`, and `scope` are required.
12
+ `token_type` must be Bearer. `expires_in` is required for token
13
+ refreshes.
14
+ )
15
+ id :smart_token_response_body
16
+
17
+ input :requested_scopes
18
+ output :id_token,
19
+ :refresh_token,
20
+ :access_token,
21
+ :expires_in,
22
+ :patient_id,
23
+ :encounter_id,
24
+ :received_scopes,
25
+ :intent
26
+ uses_request :token
27
+
28
+ run do
29
+ skip_if request.status != 200, 'Token exchange was unsuccessful'
30
+
31
+ assert_valid_json(request.response_body)
32
+ token_response_body = JSON.parse(request.response_body)
33
+
34
+ output id_token: token_response_body['id_token'],
35
+ refresh_token: token_response_body['refresh_token'],
36
+ access_token: token_response_body['access_token'],
37
+ expires_in: token_response_body['expires_in'],
38
+ patient_id: token_response_body['patient'],
39
+ encounter_id: token_response_body['encounter'],
40
+ received_scopes: token_response_body['scope'],
41
+ intent: token_response_body['intent']
42
+
43
+ validate_required_fields_present(token_response_body, ['access_token', 'token_type', 'expires_in', 'scope'])
44
+ validate_token_field_types(token_response_body)
45
+ validate_token_type(token_response_body)
46
+ check_for_missing_scopes(requested_scopes, token_response_body)
47
+
48
+ assert access_token.present?, 'Token response did not contain an access token'
49
+ assert token_response_body['token_type']&.casecmp('Bearer')&.zero?,
50
+ '`token_type` field must have a value of `Bearer`'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ module SMARTAppLaunch
2
+ class TokenResponseHeadersTest < Inferno::Test
3
+ title 'Response includes correct HTTP Cache-Control and Pragma headers'
4
+ description %(
5
+ The authorization servers response must include the HTTP Cache-Control
6
+ response header field with a value of no-store, as well as the Pragma
7
+ response header field with a value of no-cache.
8
+ )
9
+ id :smart_token_response_headers
10
+
11
+ uses_request :token
12
+
13
+ run do
14
+ skip_if request.status != 200, 'Token exchange was unsuccessful'
15
+
16
+ cc_header = request.response_header('Cache-Control')&.value
17
+
18
+ assert cc_header&.downcase&.include?('no-store'),
19
+ 'Token response must have `Cache-Control` header containing `no-store`.'
20
+
21
+ pragma_header = request.response_header('Pragma')&.value
22
+
23
+ assert pragma_header&.downcase&.include?('no-cache'),
24
+ 'Token response must have `Pragma` header containing `no-cache`.'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,141 @@
1
+ require_relative 'smart_app_launch/discovery_group'
2
+ require_relative 'smart_app_launch/standalone_launch_group'
3
+ require_relative 'smart_app_launch/ehr_launch_group'
4
+ require_relative 'smart_app_launch/openid_connect_group'
5
+ require_relative 'smart_app_launch/token_refresh_group'
6
+
7
+ module SMARTAppLaunch
8
+ class SMARTSuite < Inferno::TestSuite
9
+ id 'smart'
10
+ title 'SMART'
11
+
12
+ resume_test_route :get, '/launch' do
13
+ request.query_parameters['iss']
14
+ end
15
+
16
+ resume_test_route :get, '/redirect' do
17
+ request.query_parameters['state']
18
+ end
19
+
20
+ config options: {
21
+ redirect_uri: "#{Inferno::Application['inferno_host']}/custom/smart/redirect",
22
+ launch_uri: "#{Inferno::Application['inferno_host']}/custom/smart/launch"
23
+ }
24
+
25
+ group do
26
+ title 'Standalone Launch'
27
+
28
+ run_as_group
29
+
30
+ group from: :smart_discovery
31
+
32
+ group from: :smart_standalone_launch
33
+
34
+ group from: :smart_openid_connect,
35
+ config: {
36
+ inputs: {
37
+ id_token: { name: :standalone_id_token },
38
+ client_id: { name: :standalone_client_id },
39
+ requested_scopes: { name: :standalone_requested_scopes }
40
+ }
41
+ }
42
+
43
+ group from: :smart_token_refresh,
44
+ id: :smart_standalone_refresh_without_scopes,
45
+ title: 'SMART Token Refresh Without Scopes',
46
+ config: {
47
+ inputs: {
48
+ refresh_token: { name: :standalone_refresh_token },
49
+ client_id: { name: :standalone_client_id },
50
+ client_secret: { name: :standalone_client_secret },
51
+ received_scopes: { name: :standalone_received_scopes }
52
+ },
53
+ outputs: {
54
+ refresh_token: { name: :standalone_refresh_token },
55
+ received_scopes: { name: :standalone_received_scopes },
56
+ access_token: { name: :standalone_access_token },
57
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
58
+ expires_in: { name: :standalone_expires_in }
59
+ }
60
+ }
61
+
62
+ group from: :smart_token_refresh,
63
+ id: :smart_standalone_refresh_with_scopes,
64
+ title: 'SMART Token Refresh With Scopes',
65
+ config: {
66
+ options: { include_scopes: true },
67
+ inputs: {
68
+ refresh_token: { name: :standalone_refresh_token },
69
+ client_id: { name: :standalone_client_id },
70
+ client_secret: { name: :standalone_client_secret },
71
+ received_scopes: { name: :standalone_received_scopes }
72
+ },
73
+ outputs: {
74
+ refresh_token: { name: :standalone_refresh_token },
75
+ received_scopes: { name: :standalone_received_scopes },
76
+ access_token: { name: :standalone_access_token },
77
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
78
+ expires_in: { name: :standalone_expires_in }
79
+ }
80
+ }
81
+ end
82
+
83
+ group do
84
+ title 'EHR Launch'
85
+
86
+ run_as_group
87
+
88
+ group from: :smart_discovery
89
+
90
+ group from: :smart_ehr_launch
91
+
92
+ group from: :smart_openid_connect,
93
+ config: {
94
+ inputs: {
95
+ id_token: { name: :ehr_id_token },
96
+ client_id: { name: :ehr_client_id },
97
+ requested_scopes: { name: :standalone_requested_scopes }
98
+ }
99
+ }
100
+
101
+ group from: :smart_token_refresh,
102
+ id: :smart_ehr_refresh_without_scopes,
103
+ title: 'SMART Token Refresh Without Scopes',
104
+ config: {
105
+ inputs: {
106
+ refresh_token: { name: :ehr_refresh_token },
107
+ client_id: { name: :ehr_client_id },
108
+ client_secret: { name: :ehr_client_secret },
109
+ received_scopes: { name: :ehr_received_scopes }
110
+ },
111
+ outputs: {
112
+ refresh_token: { name: :ehr_refresh_token },
113
+ received_scopes: { name: :ehr_received_scopes },
114
+ access_token: { name: :ehr_access_token },
115
+ token_retrieval_time: { name: :ehr_token_retrieval_time },
116
+ expires_in: { name: :ehr_expires_in }
117
+ }
118
+ }
119
+
120
+ group from: :smart_token_refresh,
121
+ id: :smart_ehr_refresh_with_scopes,
122
+ title: 'SMART Token Refresh With Scopes',
123
+ config: {
124
+ options: { include_scopes: true },
125
+ inputs: {
126
+ refresh_token: { name: :ehr_refresh_token },
127
+ client_id: { name: :ehr_client_id },
128
+ client_secret: { name: :ehr_client_secret },
129
+ received_scopes: { name: :ehr_received_scopes }
130
+ },
131
+ outputs: {
132
+ refresh_token: { name: :ehr_refresh_token },
133
+ received_scopes: { name: :ehr_received_scopes },
134
+ access_token: { name: :ehr_access_token },
135
+ token_retrieval_time: { name: :ehr_token_retrieval_time },
136
+ expires_in: { name: :ehr_expires_in }
137
+ }
138
+ }
139
+ end
140
+ end
141
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_app_launch_test_kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stephen MacVicar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: inferno_core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.2.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: database_cleaner-sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: factory_bot
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '6.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '6.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-test
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.1.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.1.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.11'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.11'
111
+ description: Inferno Tests for the SMART Application Launch Framework Implementation
112
+ Guide
113
+ email:
114
+ - inferno@groups.mitre.org
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - LICENSE
120
+ - lib/smart_app_launch/app_launch_test.rb
121
+ - lib/smart_app_launch/app_redirect_test.rb
122
+ - lib/smart_app_launch/code_received_test.rb
123
+ - lib/smart_app_launch/discovery_group.rb
124
+ - lib/smart_app_launch/ehr_launch_group.rb
125
+ - lib/smart_app_launch/launch_received_test.rb
126
+ - lib/smart_app_launch/openid_connect_group.rb
127
+ - lib/smart_app_launch/openid_decode_id_token_test.rb
128
+ - lib/smart_app_launch/openid_fhir_user_claim_test.rb
129
+ - lib/smart_app_launch/openid_required_configuration_fields_test.rb
130
+ - lib/smart_app_launch/openid_retrieve_configuration_test.rb
131
+ - lib/smart_app_launch/openid_retrieve_jwks_test.rb
132
+ - lib/smart_app_launch/openid_token_header_test.rb
133
+ - lib/smart_app_launch/openid_token_payload_test.rb
134
+ - lib/smart_app_launch/standalone_launch_group.rb
135
+ - lib/smart_app_launch/token_exchange_test.rb
136
+ - lib/smart_app_launch/token_payload_validation.rb
137
+ - lib/smart_app_launch/token_refresh_body_test.rb
138
+ - lib/smart_app_launch/token_refresh_group.rb
139
+ - lib/smart_app_launch/token_refresh_test.rb
140
+ - lib/smart_app_launch/token_response_body_test.rb
141
+ - lib/smart_app_launch/token_response_headers_test.rb
142
+ - lib/smart_app_launch_test_kit.rb
143
+ homepage: https://github.com/inferno_community/smart-app-launch-test-kit
144
+ licenses:
145
+ - Apache-2.0
146
+ metadata:
147
+ homepage_uri: https://github.com/inferno_community/smart-app-launch-test-kit
148
+ source_code_uri: https://github.com/inferno_community/smart-app-launch-test-kit
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: 2.7.0
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubygems_version: 3.1.6
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Inferno Tests for the SMART Application Launch Framework Implementation Guide
168
+ test_files: []