smart_app_launch_test_kit 0.0.1

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