smart_app_launch_test_kit 0.6.1 → 0.6.3
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/config/presets/SMART_RunClientAgainstServer.json.erb +57 -9
- data/config/presets/SMART_RunServerAgainstClient_ConfidentialAsymmetric.json.erb +183 -0
- data/config/presets/SMART_RunServerAgainstClient_ConfidentialSymmetric.json.erb +157 -0
- data/config/presets/SMART_RunServerAgainstClient_Public.json.erb +155 -0
- data/lib/smart_app_launch/backend_services_invalid_client_assertion_test.rb +1 -1
- data/lib/smart_app_launch/backend_services_invalid_jwt_test.rb +1 -1
- data/lib/smart_app_launch/client_stu2_2_suite.rb +60 -19
- data/lib/smart_app_launch/client_suite/access_alca_interaction_test.rb +75 -0
- data/lib/smart_app_launch/client_suite/access_alcs_interaction_test.rb +75 -0
- data/lib/smart_app_launch/client_suite/access_alp_interaction_test.rb +75 -0
- data/lib/smart_app_launch/client_suite/access_bsca_interaction_test.rb +46 -0
- data/lib/smart_app_launch/client_suite/access_group.rb +85 -0
- data/lib/smart_app_launch/client_suite/authentication_verification.rb +86 -0
- data/lib/smart_app_launch/client_suite/authorization_request_verification_test.rb +108 -0
- data/lib/smart_app_launch/client_suite/client_descriptions.rb +114 -0
- data/lib/smart_app_launch/client_suite/client_options.rb +35 -0
- data/lib/smart_app_launch/client_suite/oidc_jwks.json +32 -0
- data/lib/smart_app_launch/client_suite/oidc_jwks.rb +27 -0
- data/lib/smart_app_launch/client_suite/registration_alca_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/registration_alca_verification_test.rb +57 -0
- data/lib/smart_app_launch/client_suite/registration_alcs_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/registration_alcs_verification_test.rb +56 -0
- data/lib/smart_app_launch/client_suite/registration_alp_group.rb +16 -0
- data/lib/smart_app_launch/client_suite/registration_alp_verification_test.rb +50 -0
- data/lib/smart_app_launch/client_suite/registration_bsca_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/registration_bsca_verification_test.rb +40 -0
- data/lib/smart_app_launch/client_suite/registration_verification.rb +58 -0
- data/lib/smart_app_launch/client_suite/token_request_alca_verification_test.rb +53 -0
- data/lib/smart_app_launch/client_suite/token_request_alcs_verification_test.rb +53 -0
- data/lib/smart_app_launch/client_suite/token_request_alp_verification_test.rb +48 -0
- data/lib/smart_app_launch/client_suite/token_request_bsca_verification_test.rb +53 -0
- data/lib/smart_app_launch/client_suite/token_request_verification.rb +116 -0
- data/lib/smart_app_launch/client_suite/{client_token_use_verification_test.rb → token_use_verification_test.rb} +1 -8
- data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +128 -41
- data/lib/smart_app_launch/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/authorization_endpoint.rb +27 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/introspection_endpoint.rb +33 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/smart_authorization_response_creation.rb +30 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/smart_introspection_response_creation.rb +46 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/smart_token_response_creation.rb +250 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/token_endpoint.rb +58 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server.rb +130 -69
- data/lib/smart_app_launch/metadata.rb +19 -14
- data/lib/smart_app_launch/tags.rb +9 -1
- data/lib/smart_app_launch/token_payload_validation.rb +2 -2
- data/lib/smart_app_launch/urls.rb +12 -0
- data/lib/smart_app_launch/version.rb +2 -2
- metadata +38 -11
- data/config/presets/SMART_RunServerAgainstClient.json.erb +0 -42
- data/lib/smart_app_launch/client_suite/client_access_group.rb +0 -26
- data/lib/smart_app_launch/client_suite/client_access_interaction_test.rb +0 -64
- data/lib/smart_app_launch/client_suite/client_registration_group.rb +0 -15
- data/lib/smart_app_launch/client_suite/client_registration_verification_test.rb +0 -52
- data/lib/smart_app_launch/client_suite/client_token_request_verification_test.rb +0 -146
- data/lib/smart_app_launch/endpoints/echoing_fhir_responder.rb +0 -52
- data/lib/smart_app_launch/endpoints/mock_smart_server/token.rb +0 -27
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../tags'
|
2
|
+
require_relative '../urls'
|
3
|
+
require_relative '../endpoints/mock_smart_server'
|
4
|
+
require_relative 'authentication_verification'
|
5
|
+
require_relative 'client_descriptions'
|
6
|
+
require_relative 'client_options'
|
7
|
+
require_relative 'token_request_verification'
|
8
|
+
|
9
|
+
module SMARTAppLaunch
|
10
|
+
class SMARTClientTokenRequestAppLaunchPublicVerification < Inferno::Test
|
11
|
+
include URLs
|
12
|
+
include AuthenticationVerification
|
13
|
+
include TokenRequestVerification
|
14
|
+
|
15
|
+
id :smart_client_token_request_alp_verification
|
16
|
+
title 'Verify SMART Token Requests'
|
17
|
+
description %(
|
18
|
+
Check that SMART token requests are conformant.
|
19
|
+
)
|
20
|
+
|
21
|
+
input :client_id,
|
22
|
+
title: 'Client Id',
|
23
|
+
type: 'text',
|
24
|
+
optional: false,
|
25
|
+
locked: true,
|
26
|
+
description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
|
27
|
+
|
28
|
+
output :smart_tokens
|
29
|
+
|
30
|
+
def client_suite_id
|
31
|
+
return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
|
32
|
+
|
33
|
+
SMARTAppLaunch::SMARTClientSTU22Suite.id
|
34
|
+
end
|
35
|
+
|
36
|
+
run do
|
37
|
+
load_tagged_requests(TOKEN_TAG, SMART_TAG, AUTHORIZATION_CODE_TAG)
|
38
|
+
skip_if requests.blank?, 'No SMART authorization code token requests made.'
|
39
|
+
load_tagged_requests(TOKEN_TAG, SMART_TAG, REFRESH_TOKEN_TAG) # verify refresh_requests as well
|
40
|
+
|
41
|
+
verify_token_requests(AUTHORIZATION_CODE_TAG, PUBLIC_TAG)
|
42
|
+
|
43
|
+
assert messages.none? { |msg|
|
44
|
+
msg[:type] == 'error'
|
45
|
+
}, 'Invalid token requests received. See messages for details.'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative '../tags'
|
2
|
+
require_relative '../urls'
|
3
|
+
require_relative '../endpoints/mock_smart_server'
|
4
|
+
require_relative 'authentication_verification'
|
5
|
+
require_relative 'client_descriptions'
|
6
|
+
require_relative 'client_options'
|
7
|
+
require_relative 'token_request_verification'
|
8
|
+
|
9
|
+
|
10
|
+
module SMARTAppLaunch
|
11
|
+
class SMARTClientTokenRequestBackendServicesConfidentialAsymmetricVerification < Inferno::Test
|
12
|
+
include URLs
|
13
|
+
include AuthenticationVerification
|
14
|
+
include TokenRequestVerification
|
15
|
+
|
16
|
+
id :smart_client_token_request_bsca_verification
|
17
|
+
title 'Verify SMART Token Requests'
|
18
|
+
description %(
|
19
|
+
Check that SMART token requests are conformant.
|
20
|
+
)
|
21
|
+
|
22
|
+
input :client_id,
|
23
|
+
title: 'Client Id',
|
24
|
+
type: 'text',
|
25
|
+
locked: true,
|
26
|
+
description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
|
27
|
+
input :smart_jwk_set,
|
28
|
+
title: 'JSON Web Key Set (JWKS)',
|
29
|
+
type: 'textarea',
|
30
|
+
locked: true,
|
31
|
+
description: INPUT_CLIENT_JWKS_DESCRIPTION_LOCKED
|
32
|
+
|
33
|
+
output :smart_tokens
|
34
|
+
|
35
|
+
def client_suite_id
|
36
|
+
return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
|
37
|
+
|
38
|
+
SMARTAppLaunch::SMARTClientSTU22Suite.id
|
39
|
+
end
|
40
|
+
|
41
|
+
run do
|
42
|
+
load_tagged_requests(TOKEN_TAG, SMART_TAG, CLIENT_CREDENTIALS_TAG)
|
43
|
+
skip_if requests.blank?, 'No SMART token requests made.'
|
44
|
+
load_tagged_requests(TOKEN_TAG, SMART_TAG, REFRESH_TOKEN_TAG) # verify refresh_requests as well (shouldn't be any)
|
45
|
+
|
46
|
+
verify_token_requests(CLIENT_CREDENTIALS_TAG, CONFIDENTIAL_ASYMMETRIC_TAG)
|
47
|
+
|
48
|
+
assert messages.none? { |msg|
|
49
|
+
msg[:type] == 'error'
|
50
|
+
}, 'Invalid token requests received. See messages for details.'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module SMARTAppLaunch
|
2
|
+
module TokenRequestVerification
|
3
|
+
|
4
|
+
def verify_token_requests(oauth_flow, authentication_approach)
|
5
|
+
jti_list = []
|
6
|
+
token_list = []
|
7
|
+
requests.each_with_index do |token_request, index|
|
8
|
+
request_params = URI.decode_www_form(token_request.request_body).to_h
|
9
|
+
request_params['grant_type'] != 'refresh_token' ?
|
10
|
+
check_request_params(request_params, oauth_flow, authentication_approach, index + 1) :
|
11
|
+
check_refresh_request_params(request_params, oauth_flow, authentication_approach, index + 1)
|
12
|
+
check_authentication(authentication_approach, token_request, request_params, jti_list, index + 1)
|
13
|
+
|
14
|
+
token_list << extract_token_from_response(token_request)
|
15
|
+
end
|
16
|
+
|
17
|
+
output smart_tokens: token_list.compact.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_request_params(params, oauth_flow, authentication_approach, request_num)
|
21
|
+
if params['grant_type'] != oauth_flow
|
22
|
+
add_message('error',
|
23
|
+
"Token request #{request_num} had an incorrect `grant_type`: expected #{oauth_flow}, " \
|
24
|
+
"but got '#{params['grant_type']}'")
|
25
|
+
end
|
26
|
+
if authentication_approach == CONFIDENTIAL_ASYMMETRIC_TAG &&
|
27
|
+
params['client_assertion_type'] != 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
|
28
|
+
add_message('error',
|
29
|
+
"Confidential asymmetric token request #{request_num} had an incorrect `client_assertion_type`: " \
|
30
|
+
"expected 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', " \
|
31
|
+
"but got '#{params['client_assertion_type']}'")
|
32
|
+
end
|
33
|
+
if oauth_flow == CLIENT_CREDENTIALS_TAG && params['scope'].blank?
|
34
|
+
add_message('error', "Client credentials token request #{request_num} did not include the requested `scope`")
|
35
|
+
end
|
36
|
+
if authentication_approach == PUBLIC_TAG && params['client_id'] != client_id
|
37
|
+
add_message('error', "Public client token request #{request_num} had an incorrect `client` value: " \
|
38
|
+
"expected '#{client_id}' but got '#{params['client_id']}'")
|
39
|
+
end
|
40
|
+
|
41
|
+
check_authorization_code_request_params(params, request_num) if oauth_flow == AUTHORIZATION_CODE_TAG
|
42
|
+
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_authorization_code_request_params(params, request_num)
|
47
|
+
if params['code'].present?
|
48
|
+
|
49
|
+
authorization_request = MockSMARTServer.authorization_request_for_code(params['code'], test_session_id)
|
50
|
+
|
51
|
+
if authorization_request.present?
|
52
|
+
authorization_body = MockSMARTServer.authorization_code_request_details(authorization_request)
|
53
|
+
|
54
|
+
if params['redirect_uri'] != authorization_body['redirect_uri']
|
55
|
+
add_message('error', "Authorization code token request #{request_num} included an incorrect " \
|
56
|
+
"`redirect_uri` value: expected '#{authorization_body['redirect_uri']} " \
|
57
|
+
"but got '#{params['redirect_uri']}'")
|
58
|
+
end
|
59
|
+
|
60
|
+
pkce_error = MockSMARTServer.pkce_error(params['code_verifier'],
|
61
|
+
authorization_body['code_challenge'],
|
62
|
+
authorization_body['code_challenge_method'])
|
63
|
+
if pkce_error.present?
|
64
|
+
add_message('error', 'Error performing pkce verification on the `code_verifier` value in ' \
|
65
|
+
"authorization code token request #{request_num}: #{pkce_error}")
|
66
|
+
end
|
67
|
+
else
|
68
|
+
add_message('error', "Authorization code token request #{request_num} included a code not " \
|
69
|
+
"issued during this test session: '#{params['code']}'")
|
70
|
+
end
|
71
|
+
else
|
72
|
+
add_message('error', "Authorization code token request #{request_num} missing a `code`")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_refresh_request_params(params, oauth_flow, authentication_approach, request_num)
|
77
|
+
if oauth_flow == CLIENT_CREDENTIALS_TAG
|
78
|
+
add_message('error',
|
79
|
+
"Invalid refresh request #{request_num} found during client_credentials flow.")
|
80
|
+
return
|
81
|
+
end
|
82
|
+
|
83
|
+
if params['grant_type'] != 'refresh_token'
|
84
|
+
add_message('error',
|
85
|
+
"Refresh request #{request_num} had an incorrect `grant_type`: expected 'refresh_token', " \
|
86
|
+
"but got '#{params['grant_type']}'")
|
87
|
+
end
|
88
|
+
if authentication_approach == CONFIDENTIAL_ASYMMETRIC_TAG &&
|
89
|
+
params['client_assertion_type'] != 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
|
90
|
+
add_message('error',
|
91
|
+
"Confidential asymmetric refresh request #{request_num} had an incorrect `client_assertion_type`: " \
|
92
|
+
"expected 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', " \
|
93
|
+
"but got '#{params['client_assertion_type']}'")
|
94
|
+
end
|
95
|
+
|
96
|
+
authorization_code = MockSMARTServer.refresh_token_to_authorization_code(params['refresh_token'])
|
97
|
+
authorization_request = MockSMARTServer.authorization_request_for_code(authorization_code, test_session_id)
|
98
|
+
if authorization_request.present?
|
99
|
+
# todo - check that the scope is a subset of the original authorization code request
|
100
|
+
else
|
101
|
+
add_message('error', "Authorization code token refresh request #{request_num} included a refresh token not " \
|
102
|
+
"issued during this test session: '#{params['refresh_token']}'")
|
103
|
+
end
|
104
|
+
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def extract_token_from_response(request)
|
109
|
+
return unless request.status == 200
|
110
|
+
|
111
|
+
JSON.parse(request.response_body)&.dig('access_token')
|
112
|
+
rescue
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -3,7 +3,6 @@ require_relative '../endpoints/mock_smart_server'
|
|
3
3
|
|
4
4
|
module SMARTAppLaunch
|
5
5
|
class SMARTClientTokenUseVerification < Inferno::Test
|
6
|
-
|
7
6
|
id :smart_client_token_use_verification
|
8
7
|
title 'Verify SMART Token Use'
|
9
8
|
description %(
|
@@ -11,11 +10,8 @@ module SMARTAppLaunch
|
|
11
10
|
authentication.
|
12
11
|
)
|
13
12
|
|
14
|
-
input :smart_tokens,
|
13
|
+
input :smart_tokens, # from :smart_client_token_request_verification
|
15
14
|
optional: true # verified in the test to return a more specific error message
|
16
|
-
input :smart_jwk_set,
|
17
|
-
optional: false,
|
18
|
-
locked: true
|
19
15
|
|
20
16
|
def access_request_tags
|
21
17
|
return config.options[:access_request_tags] if config.options[:access_request_tags].present?
|
@@ -24,9 +20,6 @@ module SMARTAppLaunch
|
|
24
20
|
end
|
25
21
|
|
26
22
|
run do
|
27
|
-
omit_if smart_jwk_set.blank?, # for re-use: mark the smart_jwk_set input as optional when importing to enable
|
28
|
-
'SMART Authentication not demonstrated as a part of this test session.'
|
29
|
-
|
30
23
|
access_requests = access_request_tags.map do |access_request_tag|
|
31
24
|
load_tagged_requests(access_request_tag).reject { |access| access.status == 401 }
|
32
25
|
end.flatten
|
@@ -7,11 +7,15 @@ client systems to the STU 2.2.0 version of the HL7® FHIR®
|
|
7
7
|
## Scope
|
8
8
|
|
9
9
|
The SMART App Launch Client Test Suite verifies that systems correctly implement
|
10
|
-
the [SMART App Launch IG](http://hl7.org/fhir/smart-app-launch/STU2.2/)
|
11
|
-
for
|
12
|
-
access to HL7® FHIR® APIs.
|
13
|
-
|
14
|
-
|
10
|
+
the aproach specified in the [SMART App Launch IG](http://hl7.org/fhir/smart-app-launch/STU2.2/)
|
11
|
+
for authorizing and potentially authenticating with a server in order to gain
|
12
|
+
access to HL7® FHIR® APIs. The suite contains options for testing clients that follow the
|
13
|
+
- [App Launch flow](https://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html), for
|
14
|
+
- Public clients not authenticating with the server.
|
15
|
+
- Confidential clients using [symmetric authentication](https://hl7.org/fhir/smart-app-launch/STU2.2/client-confidential-symmetric.html).
|
16
|
+
- Confidential clients using [asymmetric authentication](https://hl7.org/fhir/smart-app-launch/STU2.2/client-confidential-asymmetric.html).
|
17
|
+
- [Backend Services flow](https://hl7.org/fhir/smart-app-launch/STU2.2/backend-services.html),
|
18
|
+
which requires clients to use [asymmetric authentication](https://hl7.org/fhir/smart-app-launch/STU2.2/client-confidential-asymmetric.html).
|
15
19
|
|
16
20
|
These tests are a **DRAFT** intended to allow implementers to perform
|
17
21
|
preliminary checks of their systems against SMART requirements and
|
@@ -21,18 +25,20 @@ requirements and may change the test verification logic.
|
|
21
25
|
|
22
26
|
## Test Methodology
|
23
27
|
|
24
|
-
For these tests Inferno simulates a SMART server
|
25
|
-
|
26
|
-
1. Provide registration details
|
27
|
-
|
28
|
-
|
28
|
+
For these tests Inferno simulates a SMART server. Testers will
|
29
|
+
1. Choose which type of client to test during test session initialization.
|
30
|
+
1. Provide registration details specific to the chosen client type as inputs,
|
31
|
+
including authentication details and optionally a client id if a specific
|
32
|
+
one should be used.
|
33
|
+
2. Follow the appropriate SMART flow to request an access token using the
|
34
|
+
registered client id.
|
29
35
|
3. Use that access token on a FHIR API request.
|
30
36
|
|
31
37
|
The simulated server is relatively permissive in the sense that it will often
|
32
38
|
provide successful responses even when the request is not conformant. When
|
33
|
-
requesting tokens, Inferno will
|
34
|
-
the client id and
|
35
|
-
run the tests. However, these non-conformant requests will be flagged by
|
39
|
+
requesting authorization codes and access tokens, Inferno will provide one as
|
40
|
+
long as it can find the client id and verify authentication. This allows incomplete
|
41
|
+
systems to run the tests. However, these non-conformant requests will be flagged by
|
36
42
|
the tests as failures so that systems will not pass the tests without being
|
37
43
|
fully conformant.
|
38
44
|
|
@@ -40,34 +46,74 @@ fully conformant.
|
|
40
46
|
|
41
47
|
### Quick Start
|
42
48
|
|
43
|
-
|
44
|
-
any tests in this suite:
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
Depending on which type of client was selected, the following inputs must be provided
|
50
|
+
at a minimum by the tester to execute any tests in this suite:
|
51
|
+
- **SMART App Launch Redirect URI(s)** (required for all *SMART App Launch* clients):
|
52
|
+
A comma-separated list of one or more URIs that the app will sepcify as the target
|
53
|
+
of the redirect for Inferno to use when providing the authorization code.
|
54
|
+
- **SMART Confidential Symmetric Client Secret** (required for the *SMART App Launch Confidential
|
55
|
+
Symmetric* clients only): The client secret that the confidential symmetric client will send with
|
56
|
+
token requests to authenticate the client to Inferno.
|
57
|
+
- **SMART JSON Web Key Set (JWKS)** (required for *Confidential Asymmetric* clients): The SMART
|
58
|
+
client's public JSON Web Key Set including key(s) that Inferno will use to verify the signature
|
59
|
+
on incoming token requests. May be provided as either a publicly accessible url containing the
|
60
|
+
JWKS, or the raw JWKS.
|
48
61
|
|
49
62
|
The *Additional Inputs* section below describes options available to customize
|
50
63
|
the behavior of Inferno's server simulation.
|
51
64
|
|
52
65
|
### Demonstration
|
53
66
|
|
54
|
-
To try out these tests without a SMART client implementation, these tests can be
|
55
|
-
using the SMART App Launch server test suite
|
67
|
+
To try out these tests without a SMART client implementation, these tests can be demonstrated
|
68
|
+
using the SMART App Launch server test suite.
|
69
|
+
|
70
|
+
#### App Launch Demonstration
|
71
|
+
|
72
|
+
1. Start an instance of the SMART App Launch STU2.2 Client test suite and choose
|
73
|
+
*SMART App Launch* options as the SMART Client Type: Public, Confidential Symmetric,
|
74
|
+
or Confidential Asymmetric. Remember the choice for later use.
|
75
|
+
1. From the drop down in the upper left, select preset "Demo: Run Against the SMART Server Suite".
|
76
|
+
1. Click the "RUN ALL TESTS" button in the upper right and click "SUBMIT".
|
77
|
+
1. In a new tab, start an instance of the SMART App Launch STU2.2 Test Suite.
|
78
|
+
1. From the drop down in the upper left, select the "Demo: Run Against the SMART Client Suite
|
79
|
+
([security type])" preset corresponding to the client type choice made in step 1.
|
80
|
+
1. Select test group **1** Standalone Launch from the left panel, click the "RUN TESTS" button
|
81
|
+
in the upper right, and click "SUBMIT". When prompted, click the link to authorize and
|
82
|
+
the tests will run to completion.
|
83
|
+
1. Select test group **2** EHR Launch from the left panel, click the "RUN TESTS" button
|
84
|
+
in the upper right, and click "SUBMIT".
|
85
|
+
1. When prompted to launch the app, return to the Client tests and open the `launch` link
|
86
|
+
in a new tab which will open a new copy of the server tests.
|
87
|
+
1. When prompted in the new tab, click the link to authorize and the tests will run to completion.
|
88
|
+
1. Select test group **4** Token Introspection from the left panel, click the "RUN ALL TESTS" button
|
89
|
+
in the upper right, and click "SUBMIT". When prompted, click the link to authorize and
|
90
|
+
the tests will run to completion.
|
91
|
+
1. Return to the client tests and click the link to continue and complete the tests.
|
92
|
+
|
93
|
+
The client tests should pass. The server tests are expected to have errors in the Token Introspection
|
94
|
+
tests for the invalid token tests because Inferno is not able to associate the invalid token introspection
|
95
|
+
test with the client session.
|
96
|
+
|
97
|
+
#### Backend Services Demonstration
|
98
|
+
|
99
|
+
The Backend Services server tests do not make a data access request, so a simple HTTP request
|
100
|
+
generator in needed to demonstrate the Backend Services client tests. The following
|
56
101
|
steps use [Postman](https://www.postman.com/) to generate the access request using
|
57
102
|
[this collection](https://github.com/inferno-framework/smart-app-launch-test-kit/blob/main/lib/smart_app_launch/docs/demo/FHIR%20Request.postman_collection.json). Install the app and import the collection before following these
|
58
103
|
steps.
|
59
104
|
|
60
|
-
1. Start an instance of the SMART App Launch STU2.2 Client test suite
|
105
|
+
1. Start an instance of the SMART App Launch STU2.2 Client test suite and choose
|
106
|
+
*SMART Backend Services Confidential Asymmetric Client* as the SMART Client Type.
|
61
107
|
2. From the drop down in the upper left, select preset "Demo: Run Against the SMART Server Suite".
|
62
|
-
3. Click the "RUN ALL TESTS" button in the upper right and click "SUBMIT"
|
63
|
-
4. In a new tab, start an instance of the SMART App Launch STU2.2 Test Suite
|
64
|
-
5. From the drop down in the upper left, select preset "Demo: Run Against the SMART Client Suite"
|
108
|
+
3. Click the "RUN ALL TESTS" button in the upper right and click "SUBMIT".
|
109
|
+
4. In a new tab, start an instance of the SMART App Launch STU2.2 Test Suite.
|
110
|
+
5. From the drop down in the upper left, select preset "Demo: Run Against the SMART Client Suite (Confidential Asymmetric)".
|
65
111
|
6. Select test group **3** Backend Services from the left panel, click the "RUN TESTS" button
|
66
|
-
in the upper right, and click "SUBMIT"
|
112
|
+
in the upper right, and click "SUBMIT".
|
67
113
|
7. Find the access token to use for the data access request by opening test **3.2.05** Authorization
|
68
114
|
request succeeds when supplied correct information, click on the "REQUESTS" tab, clicking on the "DETAILS"
|
69
115
|
button, and expanding the "Response Body". Copy the "access_token" value, which will be a ~100 character
|
70
|
-
string of letters and numbers (e.g., eyJjbGllbnRfaWQiOiJzbWFydF9jbGllbnRfdGVzdF9kZW1vIiwiZXhwaXJhdGlvbiI6MTc0MzUxNDk4Mywibm9uY2UiOiJlZDI5MWIwNmZhMTE4OTc4In0)
|
116
|
+
string of letters and numbers (e.g., eyJjbGllbnRfaWQiOiJzbWFydF9jbGllbnRfdGVzdF9kZW1vIiwiZXhwaXJhdGlvbiI6MTc0MzUxNDk4Mywibm9uY2UiOiJlZDI5MWIwNmZhMTE4OTc4In0).
|
71
117
|
8. Open Postman and open the "FHIR Request" Collection. Click the "Variables" tab and add the copied access token
|
72
118
|
as the current value of the `bearer_token` variable. Also update the
|
73
119
|
`base_url` value for where the test is running (see details on the "Overview" tab).
|
@@ -80,31 +126,72 @@ expected as the Server tests make several intentionally invalid token requests.
|
|
80
126
|
server responds successfully to those requests when the client id can be identified, but flags them as
|
81
127
|
not conformant causing these expected failures. Because responding with an access token to non-conformant
|
82
128
|
token requests is itself not conformant there are corresponding failures on the server test in tests **3.2.02**,
|
83
|
-
**3.2.
|
84
|
-
servers support the app launch capabilities in addition to backend services.
|
129
|
+
**3.2.03**, and **3.2.04**.
|
85
130
|
|
86
131
|
### Additional Inputs
|
87
132
|
|
88
|
-
|
133
|
+
#### Additional Registration Inputs
|
134
|
+
|
135
|
+
Testers have the option to provide two additional SMART registration details:
|
89
136
|
- **Client Id**: Testers may specify a client id for Inferno to use for the test session if they
|
90
137
|
have one already configured.
|
91
|
-
- **
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
138
|
+
- **SMART App Launch URL(s)** (available for all *SMART App Launch* clients): To demonstrate an EHR
|
139
|
+
launch, provide one or more URLs, separated by commas, that Inferno can use to launch the app.
|
140
|
+
|
141
|
+
#### Inputs Controlling Token Responses
|
142
|
+
|
143
|
+
Inferno's SMART simulation does not include the details needed to populate
|
144
|
+
the token response [context data](https://hl7.org/fhir/smart-app-launch/STU2.2/scopes-and-launch-context.html)
|
145
|
+
when requested by apps using scopes during the *SMART App Launch* flow. If the tested app
|
146
|
+
needs and will request these details, the tester must provide them for Inferno
|
147
|
+
to respond with using the following inputs:
|
148
|
+
- **Launch Context** (available for all *SMART App Launch* clients): Testers can provide a JSON
|
149
|
+
array for Inferno to use as the base for building a token response on. This can include
|
150
|
+
keys like `"patient"` when the `launch/patient` scope will be requested. Note that when keys that Inferno
|
151
|
+
also populates (e.g. `access_token` or `id_token`) are included, the Inferno value will be returned.
|
152
|
+
- **FHIR User Relative Reference** (available for all *SMART App Launch* clients): Testers
|
153
|
+
can provide a FHIR relative reference (`<resource type>/<id>`) for the FHIR user record
|
154
|
+
to return with the `id_token` when the `openid` and `fhirUser` scopes are requested. If populated,
|
155
|
+
include the corresponding resource in the **Available Resources** input (See the "Inputs
|
156
|
+
Controlling FHIR Responses" section) so that it can be accessed via FHIR read.
|
157
|
+
|
158
|
+
#### Inputs Controlling FHIR Responses
|
159
|
+
The focus of this test kit is on the auth protocol, so the simulated FHIR server implemented
|
160
|
+
in this test suite is very simple. It will respond to any FHIR request with either:
|
161
|
+
- A resource from a tester-provided Bundle in the **Available Resources** input
|
162
|
+
if the request is a read matching a resource type and id found in the Bundle.
|
163
|
+
- Otherwise, the contents of the **Default FHIR Response** input, if provided.
|
164
|
+
- Otherwise, an OperationOutcome indicating no response was available.
|
165
|
+
|
166
|
+
The two inputs that control these response include:
|
167
|
+
- **Available Resources**: A FHIR Bundle of resources to make available via the
|
168
|
+
simulated FHIR sever. Each entry must contain a resource with the id element
|
169
|
+
populated. Each instance present will be available for retrieval from Inferno
|
170
|
+
at the endpoint: `<fhir-base>/<resource type>/<instance id>`. These will only
|
171
|
+
be available through the read interaction.
|
172
|
+
- **FHIR Response to Echo**: A static FHIR JSON body for Inferno to return for all FHIR requests
|
173
|
+
not covered by reads of instances in the **Available Resources** input. In this case,
|
174
|
+
the simulation is a simple echo and Inferno does not check that the response is
|
175
|
+
appropriate for the request made.
|
96
176
|
|
97
177
|
## Current Limitations
|
98
178
|
|
99
179
|
This test kit is still in draft form and does not test all of the requirements and features
|
100
|
-
described in the SMART App Launch IG for clients.
|
101
|
-
|
180
|
+
described in the SMART App Launch IG for clients.
|
181
|
+
|
182
|
+
The following sections list known gaps and limitations.
|
183
|
+
|
184
|
+
### SMART Scope Checking and Fulfilment
|
102
185
|
|
103
|
-
|
186
|
+
These tests do not verify any details about scopes, including that the
|
187
|
+
- Requested scopes are conformant, such as that they have a valid format and are consistent
|
188
|
+
between authorization and refresh token requests.
|
189
|
+
- Provided **Launch Context** input fullfils the requested data context scopes.
|
190
|
+
- Access performed is allowed by the requested scope.
|
104
191
|
|
105
192
|
### SMART Server Simulation Limitations
|
106
193
|
|
107
|
-
This test suite contains a simulation of a SMART
|
194
|
+
This test suite contains a simulation of a SMART server which is not fully
|
108
195
|
general and not all conformant clients may be able to interact with it. However, the intention
|
109
196
|
is not to prevent systems from passing for making conformant choices that Inferno's simulation
|
110
197
|
does not support. One specific example is that the SMART configuration metadata available at
|
@@ -116,6 +203,6 @@ the [github repository's issues page](https://github.com/inferno-framework/smart
|
|
116
203
|
|
117
204
|
The FHIR server simulation used to support clients in demonstrating their ability to access
|
118
205
|
FHIR APIs using access tokens obtained using the SMART flows is very limited. Testers are currently
|
119
|
-
able to provide a single static response that will be echoed for any
|
120
|
-
Inferno will never implement a fully general FHIR server simulation,
|
121
|
-
in the future based on community feedback.
|
206
|
+
able to provide a list of resources to be read and a single static response that will be echoed for any
|
207
|
+
other FHIR request made. While Inferno will never implement a fully general FHIR server simulation,
|
208
|
+
additional options, such as may be added in the future based on community feedback.
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../urls'
|
4
|
+
require_relative '../tags'
|
5
|
+
require_relative 'mock_smart_server'
|
6
|
+
|
7
|
+
module SMARTAppLaunch
|
8
|
+
class EchoingFHIRResponderEndpoint < Inferno::DSL::SuiteEndpoint
|
9
|
+
def test_run_identifier
|
10
|
+
MockSMARTServer.issued_token_to_client_id(request.headers['authorization']&.delete_prefix('Bearer '))
|
11
|
+
end
|
12
|
+
|
13
|
+
def make_response
|
14
|
+
return if response.status == 401 # set in update_result (expired token handling there)
|
15
|
+
|
16
|
+
response.content_type = 'application/fhir+json'
|
17
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
18
|
+
response.status = 200
|
19
|
+
|
20
|
+
# look for read of provided resources
|
21
|
+
read_response = tester_provided_read_response_body
|
22
|
+
if read_response.present?
|
23
|
+
response.body = read_response.to_json
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
# If the tester provided a response, echo it
|
28
|
+
# otherwise, operation outcome
|
29
|
+
echo_response = JSON.parse(result.input_json)
|
30
|
+
.find { |input| input['name'].include?('echoed_fhir_response') }
|
31
|
+
&.dig('value')
|
32
|
+
if echo_response.present?
|
33
|
+
response.body = echo_response
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
response.status = 400
|
38
|
+
response.body = FHIR::OperationOutcome.new(
|
39
|
+
issue: FHIR::OperationOutcome::Issue.new(
|
40
|
+
severity: 'fatal', code: 'required',
|
41
|
+
details: FHIR::CodeableConcept.new(text: 'No response provided to echo.')
|
42
|
+
)
|
43
|
+
).to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_result
|
47
|
+
if MockSMARTServer.request_has_expired_token?(request)
|
48
|
+
MockSMARTServer.update_response_for_expired_token(response, 'Bearer token')
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
nil # never update for now
|
53
|
+
end
|
54
|
+
|
55
|
+
def tags
|
56
|
+
[ACCESS_TAG]
|
57
|
+
end
|
58
|
+
|
59
|
+
def tester_provided_read_response_body
|
60
|
+
resource_type = request.params[:one]
|
61
|
+
id = request.params[:two]
|
62
|
+
|
63
|
+
return unless resource_type.present? && id.present?
|
64
|
+
|
65
|
+
resource_type_class =
|
66
|
+
begin
|
67
|
+
FHIR.const_get(resource_type)
|
68
|
+
rescue NameError
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
return unless resource_type_class.present?
|
72
|
+
|
73
|
+
resource_bundle = ehr_input_bundle
|
74
|
+
return unless resource_bundle.present?
|
75
|
+
|
76
|
+
find_resource_in_bundle(resource_bundle, resource_type_class, id)
|
77
|
+
end
|
78
|
+
|
79
|
+
def ehr_input_bundle
|
80
|
+
ehr_bundle_input =
|
81
|
+
JSON.parse(result.input_json).find { |input| input['name'] == 'fhir_read_resources_bundle' }&.dig('value')
|
82
|
+
ehr_bundle = FHIR.from_contents(ehr_bundle_input) if ehr_bundle_input.present?
|
83
|
+
return ehr_bundle if ehr_bundle.is_a?(FHIR::Bundle)
|
84
|
+
|
85
|
+
nil
|
86
|
+
rescue StandardError
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_resource_in_bundle(bundle, resource_type_class, id)
|
91
|
+
bundle.entry&.find do |entry|
|
92
|
+
entry.resource.is_a?(resource_type_class) && entry.resource.id == id
|
93
|
+
end&.resource
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../../tags'
|
3
|
+
require_relative 'smart_authorization_response_creation'
|
4
|
+
|
5
|
+
module SMARTAppLaunch
|
6
|
+
module MockSMARTServer
|
7
|
+
class AuthorizationEndpoint < Inferno::DSL::SuiteEndpoint
|
8
|
+
include SMARTAuthorizationResponseCreation
|
9
|
+
|
10
|
+
def test_run_identifier
|
11
|
+
request.params[:client_id]
|
12
|
+
end
|
13
|
+
|
14
|
+
def make_response
|
15
|
+
make_smart_authorization_response
|
16
|
+
end
|
17
|
+
|
18
|
+
def update_result
|
19
|
+
nil # never update for now
|
20
|
+
end
|
21
|
+
|
22
|
+
def tags
|
23
|
+
[AUTHORIZATION_TAG, AUTHORIZATION_CODE_TAG, SMART_TAG]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../../tags'
|
3
|
+
require_relative '../mock_smart_server'
|
4
|
+
require_relative 'smart_introspection_response_creation'
|
5
|
+
|
6
|
+
module SMARTAppLaunch
|
7
|
+
module MockSMARTServer
|
8
|
+
class IntrospectionEndpoint < Inferno::DSL::SuiteEndpoint
|
9
|
+
include SMARTIntrospectionResponseCreation
|
10
|
+
|
11
|
+
def test_run_identifier
|
12
|
+
MockSMARTServer.issued_token_to_client_id(request.params[:token])
|
13
|
+
end
|
14
|
+
|
15
|
+
def make_response
|
16
|
+
response.body = make_smart_introspection_response.to_json
|
17
|
+
response.headers['Cache-Control'] = 'no-store'
|
18
|
+
response.headers['Pragma'] = 'no-cache'
|
19
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
20
|
+
response.content_type = 'application/json'
|
21
|
+
response.status = 200
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_result
|
25
|
+
nil # never update for now
|
26
|
+
end
|
27
|
+
|
28
|
+
def tags
|
29
|
+
[INTROSPECTION_TAG, SMART_TAG]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|