smart_app_launch_test_kit 0.4.4 → 0.4.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 +4 -4
- data/lib/smart_app_launch/cors_metadata_request_test.rb +40 -0
- data/lib/smart_app_launch/cors_openid_fhir_user_claim_test.rb +48 -0
- data/lib/smart_app_launch/cors_token_exchange_test.rb +35 -0
- data/lib/smart_app_launch/cors_well_known_endpoint_test.rb +40 -0
- data/lib/smart_app_launch/discovery_stu2_2_group.rb +12 -0
- data/lib/smart_app_launch/ehr_launch_group_stu2_2.rb +14 -0
- data/lib/smart_app_launch/openid_connect_group_stu2_2.rb +40 -0
- data/lib/smart_app_launch/smart_stu2_2_suite.rb +10 -8
- data/lib/smart_app_launch/standalone_launch_group_stu2_2.rb +14 -0
- data/lib/smart_app_launch/token_exchange_stu2_2_test.rb +30 -0
- data/lib/smart_app_launch/token_exchange_test.rb +10 -14
- data/lib/smart_app_launch/token_introspection_access_token_group_stu2_2.rb +2 -4
- data/lib/smart_app_launch/token_introspection_group.rb +1 -2
- data/lib/smart_app_launch/token_introspection_group_stu2_2.rb +0 -21
- data/lib/smart_app_launch/token_introspection_request_group.rb +47 -36
- data/lib/smart_app_launch/token_refresh_test.rb +13 -9
- data/lib/smart_app_launch/version.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af89139a6750969a3efa3731229c4ff2a8754738f75024b10aa986cb69325dbb
|
4
|
+
data.tar.gz: 8b412a1bb5601e932bc149f0de5e5dc59aaf5a7dc5be962a98a575a378fb297e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c67c07a022b652ab979b1d5f1d857d2ffb44b63030e00f11f0d1ed5b0cede5a1371abef7263ea8e6c517acb617c33e3698bf74686fdec407eac5d37a48ceedc6
|
7
|
+
data.tar.gz: cef63d441dfd3f5b1cc2d0c902bab6632006f447b494ba39ebbb2a4c8b87d82cf457c60f0108a22f752c5ac525a94b69d962622b234901805aaf72a8f750e53f
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'url_helpers'
|
2
|
+
|
3
|
+
module SMARTAppLaunch
|
4
|
+
class CORSMetadataRequest < Inferno::Test
|
5
|
+
id :smart_cors_metadata_request
|
6
|
+
|
7
|
+
include URLHelpers
|
8
|
+
|
9
|
+
title 'SMART metadata Endpoint Enables Cross-Origin Resource Sharing (CORS)'
|
10
|
+
description %(
|
11
|
+
The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
|
12
|
+
specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
|
13
|
+
as follows:
|
14
|
+
|
15
|
+
- For requests from any origin, CORS configuration permits access to the public discovery endpoints
|
16
|
+
(.well-known/smart-configuration and metadata).
|
17
|
+
|
18
|
+
This test verifies that the metadata request is returned with the appropriate CORS header.
|
19
|
+
)
|
20
|
+
optional
|
21
|
+
|
22
|
+
input :url
|
23
|
+
|
24
|
+
fhir_client do
|
25
|
+
url :url
|
26
|
+
headers 'Origin' => Inferno::Application['inferno_host']
|
27
|
+
end
|
28
|
+
|
29
|
+
run do
|
30
|
+
fhir_get_capability_statement
|
31
|
+
|
32
|
+
assert_response_status(200)
|
33
|
+
inferno_origin = Inferno::Application['inferno_host']
|
34
|
+
cors_allow_origin = request.response_header('Access-Control-Allow-Origin')&.value
|
35
|
+
assert cors_allow_origin.present?, 'No `Access-Control-Allow-Origin` header received.'
|
36
|
+
assert cors_allow_origin == inferno_origin || cors_allow_origin == '*',
|
37
|
+
"`Access-Control-Allow-Origin` must be `#{inferno_origin}`, but received: `#{cors_allow_origin}`"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module SMARTAppLaunch
|
2
|
+
class CORSOpenIDFHIRUserClaimTest < Inferno::Test
|
3
|
+
id :smart_cors_openid_fhir_user_claim
|
4
|
+
title 'SMART FHIR User REST API Endpoint Enables Cross-Origin Resource Sharing (CORS)'
|
5
|
+
description %(
|
6
|
+
The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
|
7
|
+
specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
|
8
|
+
as follows:
|
9
|
+
|
10
|
+
- For requests from a client's registered origin(s), CORS configuration permits access to the token
|
11
|
+
endpoint and to FHIR REST API endpoints.
|
12
|
+
|
13
|
+
This test verifies that a request to the FHIR REST API endpoint for the FHIR user is returned with the appropriate
|
14
|
+
CORS header.
|
15
|
+
)
|
16
|
+
optional
|
17
|
+
|
18
|
+
input :url, :id_token_fhir_user
|
19
|
+
input :smart_credentials, type: :oauth_credentials
|
20
|
+
|
21
|
+
fhir_client do
|
22
|
+
url :url
|
23
|
+
oauth_credentials :smart_credentials
|
24
|
+
headers 'Origin' => Inferno::Application['inferno_host']
|
25
|
+
end
|
26
|
+
|
27
|
+
run do
|
28
|
+
valid_fhir_user_resource_types = ['Patient', 'Practitioner', 'RelatedPerson', 'Person']
|
29
|
+
|
30
|
+
fhir_user_segments = id_token_fhir_user.split('/')
|
31
|
+
fhir_user_resource_type = fhir_user_segments[-2]
|
32
|
+
fhir_user_id = fhir_user_segments.last
|
33
|
+
|
34
|
+
assert valid_fhir_user_resource_types.include?(fhir_user_resource_type),
|
35
|
+
"ID token `fhirUser` claim does not refer to a valid resource type: #{id_token_fhir_user}"
|
36
|
+
|
37
|
+
fhir_read(fhir_user_resource_type, fhir_user_id)
|
38
|
+
|
39
|
+
assert_response_status(200)
|
40
|
+
|
41
|
+
inferno_origin = Inferno::Application['inferno_host']
|
42
|
+
cors_allow_origin = request.response_header('Access-Control-Allow-Origin')&.value
|
43
|
+
assert cors_allow_origin.present?, 'No `Access-Control-Allow-Origin` header received.'
|
44
|
+
assert cors_allow_origin == inferno_origin || cors_allow_origin == '*',
|
45
|
+
"`Access-Control-Allow-Origin` must be `#{inferno_origin}`, but received: `#{cors_allow_origin}`"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SMARTAppLaunch
|
2
|
+
class CORSTokenExchangeTest < Inferno::Test
|
3
|
+
title 'SMART Token Endpoint Enables Cross-Origin Resource Sharing (CORS)'
|
4
|
+
description %(
|
5
|
+
The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
|
6
|
+
specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
|
7
|
+
as follows:
|
8
|
+
|
9
|
+
- For requests from a client's registered origin(s), CORS configuration permits access to the token
|
10
|
+
endpoint
|
11
|
+
|
12
|
+
This test verifies that the token endpoint contains the appropriate CORS header in the response.
|
13
|
+
)
|
14
|
+
id :smart_cors_token_exchange
|
15
|
+
|
16
|
+
uses_request :cors_token_request
|
17
|
+
|
18
|
+
input :client_auth_type
|
19
|
+
|
20
|
+
run do
|
21
|
+
omit_if client_auth_type != 'public', %(
|
22
|
+
Client type is not public, Cross-Origin Resource Sharing (CORS) is not required to be supported for
|
23
|
+
non-public client types
|
24
|
+
)
|
25
|
+
|
26
|
+
skip_if request.status != 200, 'Previous request was unsuccessful, cannot check for CORS support'
|
27
|
+
|
28
|
+
inferno_origin = Inferno::Application['inferno_host']
|
29
|
+
cors_header = request.response_header('Access-Control-Allow-Origin')&.value
|
30
|
+
|
31
|
+
assert cors_header == inferno_origin || cors_header == '*',
|
32
|
+
"Request must have `Access-Control-Allow-Origin` header containing `#{inferno_origin}`"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'url_helpers'
|
2
|
+
|
3
|
+
module SMARTAppLaunch
|
4
|
+
class CORSWellKnownEndpointTest < Inferno::Test
|
5
|
+
include URLHelpers
|
6
|
+
|
7
|
+
title 'SMART .well-known/smart-configuration Endpoint Enables Cross-Origin Resource Sharing (CORS)'
|
8
|
+
id :smart_cors_well_known_endpoint
|
9
|
+
description %(
|
10
|
+
The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
|
11
|
+
specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
|
12
|
+
as follows:
|
13
|
+
|
14
|
+
- For requests from any origin, CORS configuration permits access to the public discovery endpoints
|
15
|
+
(.well-known/smart-configuration and metadata).
|
16
|
+
|
17
|
+
This test verifies that the .well-known/smart-configuration request is returned with the appropriate CORS header.
|
18
|
+
)
|
19
|
+
optional
|
20
|
+
|
21
|
+
input :url,
|
22
|
+
title: 'FHIR Endpoint',
|
23
|
+
description: 'URL of the FHIR endpoint used by SMART applications'
|
24
|
+
|
25
|
+
run do
|
26
|
+
well_known_configuration_url = "#{url.chomp('/')}/.well-known/smart-configuration"
|
27
|
+
inferno_origin = Inferno::Application['inferno_host']
|
28
|
+
|
29
|
+
get(well_known_configuration_url,
|
30
|
+
headers: { 'Accept' => 'application/json',
|
31
|
+
'Origin' => inferno_origin })
|
32
|
+
assert_response_status(200)
|
33
|
+
|
34
|
+
cors_allow_origin = request.response_header('Access-Control-Allow-Origin')&.value
|
35
|
+
assert cors_allow_origin.present?, 'No `Access-Control-Allow-Origin` header received.'
|
36
|
+
assert cors_allow_origin == inferno_origin || cors_allow_origin == '*',
|
37
|
+
"`Access-Control-Allow-Origin` must be `#{inferno_origin}`, but received: `#{cors_allow_origin}`"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative 'cors_metadata_request_test'
|
2
|
+
require_relative 'cors_well_known_endpoint_test'
|
3
|
+
require_relative 'discovery_stu2_group'
|
4
|
+
|
5
|
+
module SMARTAppLaunch
|
6
|
+
class DiscoverySTU22Group < DiscoverySTU2Group
|
7
|
+
id :smart_discovery_stu2_2
|
8
|
+
|
9
|
+
test from: :smart_cors_well_known_endpoint
|
10
|
+
test from: :smart_cors_metadata_request
|
11
|
+
end
|
12
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative 'ehr_launch_group_stu2'
|
2
2
|
require_relative 'token_response_body_test_stu2_2'
|
3
|
+
require_relative 'cors_token_exchange_test'
|
4
|
+
require_relative 'token_exchange_stu2_2_test'
|
3
5
|
|
4
6
|
module SMARTAppLaunch
|
5
7
|
class EHRLaunchGroupSTU22 < EHRLaunchGroupSTU2
|
@@ -46,9 +48,21 @@ module SMARTAppLaunch
|
|
46
48
|
}
|
47
49
|
)
|
48
50
|
|
51
|
+
test from: :smart_token_exchange_stu2_2
|
52
|
+
|
53
|
+
token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'smart_token_exchange' }
|
54
|
+
children[token_exchange_index] = children.pop
|
55
|
+
|
49
56
|
test from: :smart_token_response_body_stu2_2
|
50
57
|
|
51
58
|
token_response_body_index = children.find_index { |child| child.id.to_s.end_with? 'token_response_body' }
|
52
59
|
children[token_response_body_index] = children.pop
|
60
|
+
|
61
|
+
test from: :smart_cors_token_exchange,
|
62
|
+
config: {
|
63
|
+
requests: {
|
64
|
+
cors_token_request: { name: :ehr_token }
|
65
|
+
}
|
66
|
+
}
|
53
67
|
end
|
54
68
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'cors_openid_fhir_user_claim_test'
|
2
|
+
require_relative 'openid_connect_group'
|
3
|
+
|
4
|
+
module SMARTAppLaunch
|
5
|
+
class OpenIDConnectGroupSTU22 < OpenIDConnectGroup
|
6
|
+
id :smart_openid_connect_stu2_2
|
7
|
+
title 'OpenID Connect'
|
8
|
+
short_description 'Demonstrate the ability to authenticate users with OpenID Connect.'
|
9
|
+
|
10
|
+
description %(
|
11
|
+
# Background
|
12
|
+
|
13
|
+
OpenID Connect (OIDC) provides the ability to verify the identity of the
|
14
|
+
authorizing user. Within the [SMART App Launch
|
15
|
+
Framework](https://www.hl7.org/fhir/smart-app-launch/STU2.2/), Applications can
|
16
|
+
request an `id_token` be provided with by including the `openid fhirUser`
|
17
|
+
scopes when requesting authorization.
|
18
|
+
|
19
|
+
# Test Methodology
|
20
|
+
|
21
|
+
This sequence validates the id token returned as part of the OAuth 2.0
|
22
|
+
token response. Once the token is decoded, the server's OIDC configuration
|
23
|
+
is retrieved from its well-known configuration endpoint. This
|
24
|
+
configuration is checked to ensure that all required fields are present.
|
25
|
+
Next the keys used to cryptographically sign the id token are retrieved
|
26
|
+
from the url contained in the OIDC configuration. Then the header,
|
27
|
+
payload, and signature of the id token are validated. Finally, the FHIR
|
28
|
+
resource from the `fhirUser` claim in the id token is fetched from the
|
29
|
+
FHIR server.
|
30
|
+
|
31
|
+
For more information see:
|
32
|
+
|
33
|
+
* [SMART App Launch Framework](https://www.hl7.org/fhir/smart-app-launch/STU2.2/)
|
34
|
+
* [Scopes for requesting identity data](https://www.hl7.org/fhir/smart-app-launch/STU2.2/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
|
35
|
+
* [Apps Requesting Authorization](https://www.hl7.org/fhir/smart-app-launch/STU2.2/index.html#step-1-app-asks-for-authorization)
|
36
|
+
* [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
|
37
|
+
)
|
38
|
+
test from: :smart_cors_openid_fhir_user_claim
|
39
|
+
end
|
40
|
+
end
|
@@ -2,12 +2,11 @@ require 'tls_test_kit'
|
|
2
2
|
|
3
3
|
require_relative 'jwks'
|
4
4
|
require_relative 'version'
|
5
|
-
require_relative '
|
5
|
+
require_relative 'discovery_stu2_2_group'
|
6
6
|
require_relative 'standalone_launch_group_stu2_2'
|
7
7
|
require_relative 'ehr_launch_group_stu2_2'
|
8
|
-
require_relative '
|
8
|
+
require_relative 'openid_connect_group_stu2_2'
|
9
9
|
require_relative 'token_introspection_group_stu2_2'
|
10
|
-
require_relative 'token_refresh_stu2_group'
|
11
10
|
require_relative 'backend_services_authorization_group'
|
12
11
|
|
13
12
|
module SMARTAppLaunch
|
@@ -55,6 +54,9 @@ module SMARTAppLaunch
|
|
55
54
|
following JWK Set URL:
|
56
55
|
|
57
56
|
* `#{Inferno::Application[:base_url]}/custom/smart_stu2_2/.well-known/jwks.json`
|
57
|
+
|
58
|
+
**NOTE:** This suite does not currently test [CORS
|
59
|
+
support](http://hl7.org/fhir/smart-app-launch/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support).
|
58
60
|
DESCRIPTION
|
59
61
|
|
60
62
|
input_instructions %(
|
@@ -89,10 +91,10 @@ module SMARTAppLaunch
|
|
89
91
|
|
90
92
|
run_as_group
|
91
93
|
|
92
|
-
group from: :
|
94
|
+
group from: :smart_discovery_stu2_2
|
93
95
|
group from: :smart_standalone_launch_stu2_2
|
94
96
|
|
95
|
-
group from: :
|
97
|
+
group from: :smart_openid_connect_stu2_2,
|
96
98
|
config: {
|
97
99
|
inputs: {
|
98
100
|
id_token: { name: :standalone_id_token },
|
@@ -164,11 +166,11 @@ module SMARTAppLaunch
|
|
164
166
|
|
165
167
|
run_as_group
|
166
168
|
|
167
|
-
group from: :
|
169
|
+
group from: :smart_discovery_stu2_2
|
168
170
|
|
169
171
|
group from: :smart_ehr_launch_stu2_2
|
170
172
|
|
171
|
-
group from: :
|
173
|
+
group from: :smart_openid_connect_stu2_2,
|
172
174
|
config: {
|
173
175
|
inputs: {
|
174
176
|
id_token: { name: :ehr_id_token },
|
@@ -234,7 +236,7 @@ module SMARTAppLaunch
|
|
234
236
|
|
235
237
|
run_as_group
|
236
238
|
|
237
|
-
group from: :
|
239
|
+
group from: :smart_discovery_stu2_2
|
238
240
|
group from: :backend_services_authorization
|
239
241
|
end
|
240
242
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative 'token_response_body_test_stu2_2'
|
2
2
|
require_relative 'standalone_launch_group_stu2'
|
3
|
+
require_relative 'cors_token_exchange_test'
|
4
|
+
require_relative 'token_exchange_stu2_2_test'
|
3
5
|
|
4
6
|
module SMARTAppLaunch
|
5
7
|
class StandaloneLaunchGroupSTU22 < StandaloneLaunchGroupSTU2
|
@@ -44,9 +46,21 @@ module SMARTAppLaunch
|
|
44
46
|
}
|
45
47
|
)
|
46
48
|
|
49
|
+
test from: :smart_token_exchange_stu2_2
|
50
|
+
|
51
|
+
token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'token_exchange' }
|
52
|
+
children[token_exchange_index] = children.pop
|
53
|
+
|
47
54
|
test from: :smart_token_response_body_stu2_2
|
48
55
|
|
49
56
|
token_response_body_index = children.find_index { |child| child.id.to_s.end_with? 'token_response_body' }
|
50
57
|
children[token_response_body_index] = children.pop
|
58
|
+
|
59
|
+
test from: :smart_cors_token_exchange,
|
60
|
+
config: {
|
61
|
+
requests: {
|
62
|
+
cors_token_request: { name: :standalone_token }
|
63
|
+
}
|
64
|
+
}
|
51
65
|
end
|
52
66
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'token_exchange_stu2_test'
|
2
|
+
|
3
|
+
module SMARTAppLaunch
|
4
|
+
class TokenExchangeSTU22Test < TokenExchangeSTU2Test
|
5
|
+
id :smart_token_exchange_stu2_2
|
6
|
+
|
7
|
+
def add_credentials_to_request(oauth2_params, oauth2_headers)
|
8
|
+
if client_auth_type == 'confidential_symmetric'
|
9
|
+
assert client_secret.present?,
|
10
|
+
'A client secret must be provided when using confidential symmetric client authentication.'
|
11
|
+
|
12
|
+
client_credentials = "#{client_id}:#{client_secret}"
|
13
|
+
oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
|
14
|
+
elsif client_auth_type == 'public'
|
15
|
+
oauth2_params[:client_id] = client_id
|
16
|
+
oauth2_headers['Origin'] = Inferno::Application['inferno_host']
|
17
|
+
else
|
18
|
+
oauth2_params.merge!(
|
19
|
+
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
20
|
+
client_assertion: ClientAssertionBuilder.build(
|
21
|
+
iss: client_id,
|
22
|
+
sub: client_id,
|
23
|
+
aud: smart_token_url,
|
24
|
+
client_auth_encryption_method:
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -51,7 +51,7 @@ module SMARTAppLaunch
|
|
51
51
|
skip_if request.query_parameters['error'].present?, 'Error during authorization request'
|
52
52
|
|
53
53
|
oauth2_params = {
|
54
|
-
code
|
54
|
+
code:,
|
55
55
|
redirect_uri: config.options[:redirect_uri],
|
56
56
|
grant_type: 'authorization_code'
|
57
57
|
}
|
@@ -59,9 +59,7 @@ module SMARTAppLaunch
|
|
59
59
|
|
60
60
|
add_credentials_to_request(oauth2_params, oauth2_headers)
|
61
61
|
|
62
|
-
if use_pkce == 'true'
|
63
|
-
oauth2_params[:code_verifier] = pkce_code_verifier
|
64
|
-
end
|
62
|
+
oauth2_params[:code_verifier] = pkce_code_verifier if use_pkce == 'true'
|
65
63
|
|
66
64
|
post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
|
67
65
|
|
@@ -72,17 +70,15 @@ module SMARTAppLaunch
|
|
72
70
|
|
73
71
|
token_response_body = JSON.parse(request.response_body)
|
74
72
|
|
75
|
-
|
76
73
|
output smart_credentials: {
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
74
|
+
refresh_token: token_response_body['refresh_token'],
|
75
|
+
access_token: token_response_body['access_token'],
|
76
|
+
expires_in: token_response_body['expires_in'],
|
77
|
+
client_id:,
|
78
|
+
client_secret:,
|
79
|
+
token_retrieval_time:,
|
80
|
+
token_url: smart_token_url
|
81
|
+
}.to_json
|
86
82
|
end
|
87
83
|
end
|
88
84
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require_relative 'standalone_launch_group_stu2_2'
|
2
2
|
|
3
3
|
module SMARTAppLaunch
|
4
|
-
class SMARTTokenIntrospectionAccessTokenGroupSTU22 <
|
4
|
+
class SMARTTokenIntrospectionAccessTokenGroupSTU22 < Inferno::TestGroup
|
5
5
|
title 'Request New Access Token to Introspect'
|
6
6
|
run_as_group
|
7
7
|
|
@@ -20,9 +20,7 @@ module SMARTAppLaunch
|
|
20
20
|
Register Inferno as a Standalone SMART App and provide the registration details below.
|
21
21
|
)
|
22
22
|
|
23
|
+
group from: :smart_discovery_stu2_2
|
23
24
|
group from: :smart_standalone_launch_stu2_2
|
24
|
-
|
25
|
-
standalone_launch_index = children.find_index { |child| child.id.to_s.end_with? 'standalone_launch_stu2' }
|
26
|
-
children[standalone_launch_index] = children.pop
|
27
25
|
end
|
28
26
|
end
|
@@ -57,13 +57,12 @@ module SMARTAppLaunch
|
|
57
57
|
|
58
58
|
If the introspection endpoint is protected, testers must enter their own HTTP Authorization header for the introspection request. See
|
59
59
|
[RFC 7616 The 'Basic' HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common
|
60
|
-
approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
|
60
|
+
approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
|
61
61
|
server to complete the introspection request.
|
62
62
|
|
63
63
|
**Note:** For both the Authorization header and request parameters, user-input
|
64
64
|
values will be sent exactly as entered and therefore the tester must
|
65
65
|
URI-encode any appropriate values.
|
66
66
|
)
|
67
|
-
|
68
67
|
end
|
69
68
|
end
|
@@ -43,26 +43,5 @@ module SMARTAppLaunch
|
|
43
43
|
|
44
44
|
access_token_group_index = children.find_index { |child| child.id.to_s.end_with? 'access_token_group' }
|
45
45
|
children[access_token_group_index] = children.pop
|
46
|
-
|
47
|
-
input_order :url, :standalone_client_id, :standalone_client_secret,
|
48
|
-
:authorization_method, :use_pkce, :pkce_code_challenge_method,
|
49
|
-
:standalone_requested_scopes, :client_auth_encryption_method,
|
50
|
-
:client_auth_type, :custom_authorization_header,
|
51
|
-
:optional_introspection_request_params
|
52
|
-
input_instructions %(
|
53
|
-
Executing tests at this level will run all three Token Introspection groups back-to-back. If test groups need
|
54
|
-
to be run independently, exit this window and select a specific test group instead.
|
55
|
-
|
56
|
-
These tests are currently designed such that the token introspection URL must be present in the SMART well-known endpoint.
|
57
|
-
|
58
|
-
If the introspection endpoint is protected, testers must enter their own HTTP Authorization header for the introspection request. See
|
59
|
-
[RFC 7616 The 'Basic' HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common
|
60
|
-
approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
|
61
|
-
server to complete the introspection request.
|
62
|
-
|
63
|
-
**Note:** For both the Authorization header and request parameters, user-input
|
64
|
-
values will be sent exactly as entered and therefore the tester must
|
65
|
-
URI-encode any appropriate values.
|
66
|
-
)
|
67
46
|
end
|
68
47
|
end
|
@@ -11,42 +11,44 @@ module SMARTAppLaunch
|
|
11
11
|
id :smart_token_introspection_request_group
|
12
12
|
description %(
|
13
13
|
This group of tests executes the token introspection requests and ensures the correct HTTP response is returned
|
14
|
-
but does not validate the contents of the token introspection response.
|
14
|
+
but does not validate the contents of the token introspection response.
|
15
15
|
|
16
|
-
If Inferno cannot reasonably be configured to be authorized to access the token introspection endpoint, these tests
|
16
|
+
If Inferno cannot reasonably be configured to be authorized to access the token introspection endpoint, these tests
|
17
17
|
can be skipped. Instead, an out-of-band token introspection request must be completed and the response body
|
18
18
|
manually provided as input for the Validate Introspection Response test group.
|
19
19
|
)
|
20
20
|
|
21
21
|
input_instructions %(
|
22
|
-
If the Request New Access Token group was executed, the access token input will auto-populate with that token.
|
23
|
-
Otherwise an active access token needs to be obtained out-of-band and input.
|
24
|
-
|
25
|
-
Per [RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2), "the definition of an active token is
|
26
|
-
currently dependent upon the authorization server, but this is commonly a token that has been issued by this
|
27
|
-
authorization server, is not expired, has not been revoked, and is valid for use at the protected resource making
|
22
|
+
If the Request New Access Token group was executed, the access token input will auto-populate with that token.
|
23
|
+
Otherwise an active access token needs to be obtained out-of-band and input.
|
24
|
+
|
25
|
+
Per [RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2), "the definition of an active token is
|
26
|
+
currently dependent upon the authorization server, but this is commonly a token that has been issued by this
|
27
|
+
authorization server, is not expired, has not been revoked, and is valid for use at the protected resource making
|
28
28
|
the introspection call."
|
29
29
|
|
30
30
|
If the introspection endpoint is protected, testers must enter their own HTTP Authorization header for the introspection request. See
|
31
31
|
[RFC 7616 The 'Basic' HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common
|
32
|
-
approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
|
32
|
+
approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
|
33
33
|
server to complete the introspection request.
|
34
34
|
|
35
35
|
**Note:** For both the Authorization header and request parameters, user-input
|
36
36
|
values will be sent exactly as entered and therefore the tester must URI-encode any appropriate values.
|
37
37
|
)
|
38
38
|
|
39
|
-
input :well_known_introspection_url,
|
40
|
-
title: 'Token Introspection Endpoint URL',
|
39
|
+
input :well_known_introspection_url,
|
40
|
+
title: 'Token Introspection Endpoint URL',
|
41
41
|
description: 'The complete URL of the token introspection endpoint.'
|
42
42
|
|
43
43
|
input :custom_authorization_header,
|
44
|
-
title: 'HTTP
|
44
|
+
title: 'Custom HTTP Headers for Introspection Request',
|
45
45
|
type: 'textarea',
|
46
46
|
optional: true,
|
47
47
|
description: %(
|
48
|
-
|
49
|
-
|
48
|
+
Add custom headers for the introspection request by adding each header's name and value with a new line
|
49
|
+
between each header.
|
50
|
+
Ex:
|
51
|
+
<Header 1 Name>: <Value 1>
|
50
52
|
)
|
51
53
|
|
52
54
|
input :optional_introspection_request_params,
|
@@ -54,69 +56,78 @@ module SMARTAppLaunch
|
|
54
56
|
type: 'textarea',
|
55
57
|
optional: true,
|
56
58
|
description: %(
|
57
|
-
Any additional parameters to append to the request body, separated by &. Example: 'param1=abc¶m2=def'
|
59
|
+
Any additional parameters to append to the request body, separated by &. Example: 'param1=abc¶m2=def'
|
58
60
|
)
|
59
61
|
|
60
62
|
test do
|
61
63
|
title 'Token introspection endpoint returns a response when provided an active token'
|
62
64
|
description %(
|
63
65
|
This test will execute a token introspection request for an active token and ensure a 200 status and valid JSON
|
64
|
-
body are returned in the response.
|
66
|
+
body are returned in the response.
|
65
67
|
)
|
66
68
|
|
67
|
-
input :standalone_access_token,
|
69
|
+
input :standalone_access_token,
|
68
70
|
title: 'Access Token',
|
69
71
|
description: 'The access token to be introspected. MUST be active.'
|
70
72
|
|
71
|
-
|
72
73
|
output :active_token_introspection_response_body
|
73
74
|
|
74
75
|
run do
|
75
|
-
|
76
76
|
# If this is being chained from an earlier test, it might be blank if not present in the well-known endpoint
|
77
77
|
skip_if well_known_introspection_url.nil?, 'No introspection URL present in SMART well-known endpoint.'
|
78
78
|
|
79
|
-
headers = {'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'}
|
79
|
+
headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' }
|
80
80
|
body = "token=#{standalone_access_token}"
|
81
81
|
|
82
82
|
if custom_authorization_header.present?
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
custom_headers = custom_authorization_header.split("\n")
|
84
|
+
custom_headers.each do |custom_header|
|
85
|
+
parsed_header = custom_header.split(':', 2)
|
86
|
+
assert parsed_header.length == 2,
|
87
|
+
'Incorrect custom HTTP header format input, expected: "<header name>: <header value>"'
|
88
|
+
headers[parsed_header[0]] = parsed_header[1].strip
|
89
|
+
end
|
86
90
|
end
|
87
91
|
|
88
|
-
if optional_introspection_request_params.present?
|
89
|
-
body += "&#{optional_introspection_request_params}"
|
90
|
-
end
|
92
|
+
body += "&#{optional_introspection_request_params}" if optional_introspection_request_params.present?
|
91
93
|
|
92
|
-
post(well_known_introspection_url, body
|
94
|
+
post(well_known_introspection_url, body:, headers:)
|
93
95
|
|
94
96
|
assert_response_status(200)
|
95
97
|
output active_token_introspection_response_body: request.response_body
|
96
98
|
end
|
97
|
-
|
98
99
|
end
|
99
100
|
|
100
|
-
test do
|
101
|
+
test do
|
101
102
|
title 'Token introspection endpoint returns a response when provided an invalid token'
|
102
103
|
description %(
|
103
104
|
This test will execute a token introspection request for an invalid token and ensure a 200 status and valid JSON
|
104
|
-
body are returned in response.
|
105
|
+
body are returned in response.
|
105
106
|
)
|
106
107
|
|
107
108
|
output :invalid_token_introspection_response_body
|
108
109
|
run do
|
109
|
-
|
110
110
|
# If this is being chained from an earlier test, it might be blank if not present in the well-known endpoint
|
111
111
|
skip_if well_known_introspection_url.nil?, 'No introspection URL present in SMART well-known endpoint.'
|
112
112
|
|
113
|
-
headers = {'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'}
|
114
|
-
body =
|
115
|
-
|
116
|
-
|
113
|
+
headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' }
|
114
|
+
body = 'token=invalid_token_value'
|
115
|
+
|
116
|
+
if custom_authorization_header.present?
|
117
|
+
custom_headers = custom_authorization_header.split("\n")
|
118
|
+
custom_headers.each do |custom_header|
|
119
|
+
parsed_header = custom_header.split(':', 2)
|
120
|
+
assert parsed_header.length == 2,
|
121
|
+
'Incorrect custom HTTP header format input, expected: "<header name>: <header value>"'
|
122
|
+
headers[parsed_header[0]] = parsed_header[1].strip
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
post(well_known_introspection_url, body:, headers:)
|
127
|
+
|
117
128
|
assert_response_status(200)
|
118
129
|
output invalid_token_introspection_response_body: request.response_body
|
119
130
|
end
|
120
131
|
end
|
121
132
|
end
|
122
|
-
end
|
133
|
+
end
|
@@ -30,6 +30,10 @@ module SMARTAppLaunch
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
+
def make_auth_token_request(smart_token_url, oauth2_params, oauth2_headers)
|
34
|
+
post(smart_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
|
35
|
+
end
|
36
|
+
|
33
37
|
run do
|
34
38
|
skip_if refresh_token.blank?
|
35
39
|
|
@@ -43,7 +47,7 @@ module SMARTAppLaunch
|
|
43
47
|
|
44
48
|
add_credentials_to_request(oauth2_headers, oauth2_params)
|
45
49
|
|
46
|
-
|
50
|
+
make_auth_token_request(smart_token_url, oauth2_params, oauth2_headers)
|
47
51
|
|
48
52
|
assert_response_status(200)
|
49
53
|
assert_valid_json(request.response_body)
|
@@ -52,14 +56,14 @@ module SMARTAppLaunch
|
|
52
56
|
|
53
57
|
token_response_body = JSON.parse(request.response_body)
|
54
58
|
output smart_credentials: {
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
refresh_token: token_response_body['refresh_token'].presence || refresh_token,
|
60
|
+
access_token: token_response_body['access_token'],
|
61
|
+
expires_in: token_response_body['expires_in'],
|
62
|
+
client_id:,
|
63
|
+
client_secret:,
|
64
|
+
token_retrieval_time:,
|
65
|
+
token_url: smart_token_url
|
66
|
+
}.to_json
|
63
67
|
end
|
64
68
|
end
|
65
69
|
end
|
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.4.
|
4
|
+
version: 0.4.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen MacVicar
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
11
|
+
date: 2024-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inferno_core
|
@@ -157,7 +157,12 @@ files:
|
|
157
157
|
- lib/smart_app_launch/backend_services_invalid_jwt_test.rb
|
158
158
|
- lib/smart_app_launch/client_assertion_builder.rb
|
159
159
|
- lib/smart_app_launch/code_received_test.rb
|
160
|
+
- lib/smart_app_launch/cors_metadata_request_test.rb
|
161
|
+
- lib/smart_app_launch/cors_openid_fhir_user_claim_test.rb
|
162
|
+
- lib/smart_app_launch/cors_token_exchange_test.rb
|
163
|
+
- lib/smart_app_launch/cors_well_known_endpoint_test.rb
|
160
164
|
- lib/smart_app_launch/discovery_stu1_group.rb
|
165
|
+
- lib/smart_app_launch/discovery_stu2_2_group.rb
|
161
166
|
- lib/smart_app_launch/discovery_stu2_group.rb
|
162
167
|
- lib/smart_app_launch/ehr_launch_group.rb
|
163
168
|
- lib/smart_app_launch/ehr_launch_group_stu2.rb
|
@@ -165,6 +170,7 @@ files:
|
|
165
170
|
- lib/smart_app_launch/jwks.rb
|
166
171
|
- lib/smart_app_launch/launch_received_test.rb
|
167
172
|
- lib/smart_app_launch/openid_connect_group.rb
|
173
|
+
- lib/smart_app_launch/openid_connect_group_stu2_2.rb
|
168
174
|
- lib/smart_app_launch/openid_decode_id_token_test.rb
|
169
175
|
- lib/smart_app_launch/openid_fhir_user_claim_test.rb
|
170
176
|
- lib/smart_app_launch/openid_required_configuration_fields_test.rb
|
@@ -190,6 +196,7 @@ files:
|
|
190
196
|
- lib/smart_app_launch/standalone_launch_group.rb
|
191
197
|
- lib/smart_app_launch/standalone_launch_group_stu2.rb
|
192
198
|
- lib/smart_app_launch/standalone_launch_group_stu2_2.rb
|
199
|
+
- lib/smart_app_launch/token_exchange_stu2_2_test.rb
|
193
200
|
- lib/smart_app_launch/token_exchange_stu2_test.rb
|
194
201
|
- lib/smart_app_launch/token_exchange_test.rb
|
195
202
|
- lib/smart_app_launch/token_introspection_access_token_group.rb
|
@@ -234,7 +241,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
234
241
|
- !ruby/object:Gem::Version
|
235
242
|
version: '0'
|
236
243
|
requirements: []
|
237
|
-
rubygems_version: 3.5.
|
244
|
+
rubygems_version: 3.5.7
|
238
245
|
signing_key:
|
239
246
|
specification_version: 4
|
240
247
|
summary: Inferno Tests for the SMART Application Launch Framework Implementation Guide
|