smart_app_launch_test_kit 0.4.4 → 0.4.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|