smart_app_launch_test_kit 0.6.0 → 0.6.2

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/SMART_RunClientAgainstServer.json.erb +79 -0
  3. data/config/presets/SMART_RunServerAgainstClient_ConfidentialAsymmetric.json.erb +183 -0
  4. data/config/presets/SMART_RunServerAgainstClient_ConfidentialSymmetric.json.erb +157 -0
  5. data/config/presets/SMART_RunServerAgainstClient_Public.json.erb +155 -0
  6. data/lib/smart_app_launch/backend_services_authorization_group.rb +0 -2
  7. data/lib/smart_app_launch/backend_services_authorization_request_success_test.rb +5 -2
  8. data/lib/smart_app_launch/backend_services_authorization_response_body_test.rb +6 -2
  9. data/lib/smart_app_launch/backend_services_invalid_client_assertion_test.rb +1 -1
  10. data/lib/smart_app_launch/backend_services_invalid_jwt_test.rb +1 -1
  11. data/lib/smart_app_launch/client_stu2_2_suite.rb +120 -0
  12. data/lib/smart_app_launch/client_suite/access_alca_interaction_test.rb +75 -0
  13. data/lib/smart_app_launch/client_suite/access_alcs_interaction_test.rb +75 -0
  14. data/lib/smart_app_launch/client_suite/access_alp_interaction_test.rb +75 -0
  15. data/lib/smart_app_launch/client_suite/access_bsca_interaction_test.rb +46 -0
  16. data/lib/smart_app_launch/client_suite/access_group.rb +85 -0
  17. data/lib/smart_app_launch/client_suite/authentication_verification.rb +86 -0
  18. data/lib/smart_app_launch/client_suite/authorization_request_verification_test.rb +108 -0
  19. data/lib/smart_app_launch/client_suite/client_descriptions.rb +114 -0
  20. data/lib/smart_app_launch/client_suite/client_options.rb +35 -0
  21. data/lib/smart_app_launch/client_suite/oidc_jwks.json +32 -0
  22. data/lib/smart_app_launch/client_suite/oidc_jwks.rb +27 -0
  23. data/lib/smart_app_launch/client_suite/registration_alca_group.rb +15 -0
  24. data/lib/smart_app_launch/client_suite/registration_alca_verification_test.rb +57 -0
  25. data/lib/smart_app_launch/client_suite/registration_alcs_group.rb +15 -0
  26. data/lib/smart_app_launch/client_suite/registration_alcs_verification_test.rb +56 -0
  27. data/lib/smart_app_launch/client_suite/registration_alp_group.rb +16 -0
  28. data/lib/smart_app_launch/client_suite/registration_alp_verification_test.rb +50 -0
  29. data/lib/smart_app_launch/client_suite/registration_bsca_group.rb +15 -0
  30. data/lib/smart_app_launch/client_suite/registration_bsca_verification_test.rb +40 -0
  31. data/lib/smart_app_launch/client_suite/registration_verification.rb +58 -0
  32. data/lib/smart_app_launch/client_suite/token_request_alca_verification_test.rb +53 -0
  33. data/lib/smart_app_launch/client_suite/token_request_alcs_verification_test.rb +53 -0
  34. data/lib/smart_app_launch/client_suite/token_request_alp_verification_test.rb +48 -0
  35. data/lib/smart_app_launch/client_suite/token_request_bsca_verification_test.rb +53 -0
  36. data/lib/smart_app_launch/client_suite/token_request_verification.rb +116 -0
  37. data/lib/smart_app_launch/client_suite/token_use_verification_test.rb +40 -0
  38. data/lib/smart_app_launch/docs/demo/FHIR Request.postman_collection.json +81 -0
  39. data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +208 -0
  40. data/lib/smart_app_launch/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
  41. data/lib/smart_app_launch/endpoints/mock_smart_server/authorization_endpoint.rb +27 -0
  42. data/lib/smart_app_launch/endpoints/mock_smart_server/introspection_endpoint.rb +33 -0
  43. data/lib/smart_app_launch/endpoints/mock_smart_server/smart_authorization_response_creation.rb +30 -0
  44. data/lib/smart_app_launch/endpoints/mock_smart_server/smart_introspection_response_creation.rb +46 -0
  45. data/lib/smart_app_launch/endpoints/mock_smart_server/smart_token_response_creation.rb +250 -0
  46. data/lib/smart_app_launch/endpoints/mock_smart_server/token_endpoint.rb +58 -0
  47. data/lib/smart_app_launch/endpoints/mock_smart_server.rb +278 -0
  48. data/lib/smart_app_launch/metadata.rb +21 -16
  49. data/lib/smart_app_launch/smart_stu2_2_suite.rb +2 -1
  50. data/lib/smart_app_launch/smart_stu2_suite.rb +2 -1
  51. data/lib/smart_app_launch/tags.rb +15 -0
  52. data/lib/smart_app_launch/token_introspection_response_group.rb +1 -1
  53. data/lib/smart_app_launch/token_payload_validation.rb +2 -2
  54. data/lib/smart_app_launch/urls.rb +52 -0
  55. data/lib/smart_app_launch/version.rb +2 -2
  56. data/lib/smart_app_launch_test_kit.rb +1 -0
  57. metadata +45 -2
@@ -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
@@ -0,0 +1,40 @@
1
+ require_relative '../tags'
2
+ require_relative '../endpoints/mock_smart_server'
3
+
4
+ module SMARTAppLaunch
5
+ class SMARTClientTokenUseVerification < Inferno::Test
6
+ id :smart_client_token_use_verification
7
+ title 'Verify SMART Token Use'
8
+ description %(
9
+ Check that a SMART token returned to the client was used for request
10
+ authentication.
11
+ )
12
+
13
+ input :smart_tokens, # from :smart_client_token_request_verification
14
+ optional: true # verified in the test to return a more specific error message
15
+
16
+ def access_request_tags
17
+ return config.options[:access_request_tags] if config.options[:access_request_tags].present?
18
+
19
+ [ACCESS_TAG]
20
+ end
21
+
22
+ run do
23
+ access_requests = access_request_tags.map do |access_request_tag|
24
+ load_tagged_requests(access_request_tag).reject { |access| access.status == 401 }
25
+ end.flatten
26
+ obtained_tokens = smart_tokens&.split("\n")
27
+
28
+ skip_if obtained_tokens.blank?, 'No token requests made.'
29
+ skip_if access_requests.blank?, 'No successful access requests made.'
30
+
31
+ used_tokens = access_requests.map do |access_request|
32
+ access_request.request_headers.find do |header|
33
+ header.name.downcase == 'authorization'
34
+ end&.value&.delete_prefix('Bearer ')
35
+ end.compact
36
+
37
+ assert (used_tokens & obtained_tokens).present?, 'Returned tokens never used in any requests.'
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,81 @@
1
+ {
2
+ "info": {
3
+ "_postman_id": "22f52416-c6ae-4ffc-a388-54616465d149",
4
+ "name": "FHIR Request",
5
+ "description": "Make a simple FHIR request with a specific bearer token. Useful for security client tests like SMART and UDAP.\n\n- base_url: points to a running instance of inferno. Typical values will be\n \n - Inferno production: [https://inferno.healthit.gov/suites](https://inferno.healthit.gov/suites)\n \n - Inferno QA: [https://inferno-qa.healthit.gov/suites](https://inferno-qa.healthit.gov/suites)\n \n - Local docker: [http://localhost](http://localhost)\n \n - Local development: [http://localhost:4567](http://localhost:4567)",
6
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7
+ "_exporter_id": "32597978"
8
+ },
9
+ "item": [
10
+ {
11
+ "name": "Patient Read",
12
+ "request": {
13
+ "auth": {
14
+ "type": "bearer",
15
+ "bearer": [
16
+ {
17
+ "key": "token",
18
+ "value": "{{bearer_token}}",
19
+ "type": "string"
20
+ }
21
+ ]
22
+ },
23
+ "method": "GET",
24
+ "header": [],
25
+ "url": {
26
+ "raw": "{{base_url}}/custom/{{target_suite}}/fhir/Patient/example",
27
+ "host": [
28
+ "{{base_url}}"
29
+ ],
30
+ "path": [
31
+ "custom",
32
+ "{{target_suite}}",
33
+ "fhir",
34
+ "Patient",
35
+ "example"
36
+ ]
37
+ }
38
+ },
39
+ "response": []
40
+ }
41
+ ],
42
+ "event": [
43
+ {
44
+ "listen": "prerequest",
45
+ "script": {
46
+ "type": "text/javascript",
47
+ "packages": {},
48
+ "exec": [
49
+ ""
50
+ ]
51
+ }
52
+ },
53
+ {
54
+ "listen": "test",
55
+ "script": {
56
+ "type": "text/javascript",
57
+ "packages": {},
58
+ "exec": [
59
+ ""
60
+ ]
61
+ }
62
+ }
63
+ ],
64
+ "variable": [
65
+ {
66
+ "key": "base_url",
67
+ "value": "https://inferno.healthit.gov/suites",
68
+ "type": "string"
69
+ },
70
+ {
71
+ "key": "target_suite",
72
+ "value": "smart_client_stu2_2",
73
+ "type": "string"
74
+ },
75
+ {
76
+ "key": "bearer_token",
77
+ "value": "",
78
+ "type": "string"
79
+ }
80
+ ]
81
+ }
@@ -0,0 +1,208 @@
1
+ ## Overview
2
+
3
+ The SMART App Launch STU 2.2 Client Test Suite verifies the conformance of
4
+ client systems to the STU 2.2.0 version of the HL7® FHIR®
5
+ [SMART App Launch IG](https://hl7.org/fhir/smart-app-launch/STU2.2/).
6
+
7
+ ## Scope
8
+
9
+ The SMART App Launch Client Test Suite verifies that systems correctly implement
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).
19
+
20
+ These tests are a **DRAFT** intended to allow implementers to perform
21
+ preliminary checks of their systems against SMART requirements and
22
+ [provide feedback](https://github.com/inferno-framework/smart-app-launch-test-kit/issues)
23
+ on the tests. Future versions of these tests may verify other
24
+ requirements and may change the test verification logic.
25
+
26
+ ## Test Methodology
27
+
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.
35
+ 3. Use that access token on a FHIR API request.
36
+
37
+ The simulated server is relatively permissive in the sense that it will often
38
+ provide successful responses even when the request is not conformant. When
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
42
+ the tests as failures so that systems will not pass the tests without being
43
+ fully conformant.
44
+
45
+ ## Running the Tests
46
+
47
+ ### Quick Start
48
+
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.
61
+
62
+ The *Additional Inputs* section below describes options available to customize
63
+ the behavior of Inferno's server simulation.
64
+
65
+ ### Demonstration
66
+
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
101
+ steps use [Postman](https://www.postman.com/) to generate the access request using
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
103
+ steps.
104
+
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.
107
+ 2. From the drop down in the upper left, select preset "Demo: Run Against the SMART Server 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)".
111
+ 6. Select test group **3** Backend Services from the left panel, click the "RUN TESTS" button
112
+ in the upper right, and click "SUBMIT".
113
+ 7. Find the access token to use for the data access request by opening test **3.2.05** Authorization
114
+ request succeeds when supplied correct information, click on the "REQUESTS" tab, clicking on the "DETAILS"
115
+ button, and expanding the "Response Body". Copy the "access_token" value, which will be a ~100 character
116
+ string of letters and numbers (e.g., eyJjbGllbnRfaWQiOiJzbWFydF9jbGllbnRfdGVzdF9kZW1vIiwiZXhwaXJhdGlvbiI6MTc0MzUxNDk4Mywibm9uY2UiOiJlZDI5MWIwNmZhMTE4OTc4In0).
117
+ 8. Open Postman and open the "FHIR Request" Collection. Click the "Variables" tab and add the copied access token
118
+ as the current value of the `bearer_token` variable. Also update the
119
+ `base_url` value for where the test is running (see details on the "Overview" tab).
120
+ Save the collection.
121
+ 9. Select the "Patient Read" request and click "Send". A FHIR Patient resource should be returned.
122
+ 10. Return to the client tests and click the link to continue and complete the tests.
123
+
124
+ The client tests should pass with the exception of test **1.2.02** Verify SMART Token Requests. This is
125
+ expected as the Server tests make several intentionally invalid token requests. Inferno's simulated SMART
126
+ server responds successfully to those requests when the client id can be identified, but flags them as
127
+ not conformant causing these expected failures. Because responding with an access token to non-conformant
128
+ token requests is itself not conformant there are corresponding failures on the server test in tests **3.2.02**,
129
+ **3.2.03**, and **3.2.04**.
130
+
131
+ ### Additional Inputs
132
+
133
+ #### Additional Registration Inputs
134
+
135
+ Testers have the option to provide two additional SMART registration details:
136
+ - **Client Id**: Testers may specify a client id for Inferno to use for the test session if they
137
+ have one already configured.
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.
176
+
177
+ ## Current Limitations
178
+
179
+ This test kit is still in draft form and does not test all of the requirements and features
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
185
+
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.
191
+
192
+ ### SMART Server Simulation Limitations
193
+
194
+ This test suite contains a simulation of a SMART server which is not fully
195
+ general and not all conformant clients may be able to interact with it. However, the intention
196
+ is not to prevent systems from passing for making conformant choices that Inferno's simulation
197
+ does not support. One specific example is that the SMART configuration metadata available at
198
+ `.well-known/smart-configuration` for the simulated server is fixed and cannot be changed by
199
+ testers at this time. Please report any issues that prevent conformant systems from passing in
200
+ the [github repository's issues page](https://github.com/inferno-framework/smart-app-launch-test-kit/issues/).
201
+
202
+ ### FHIR Server Simulation Limitations
203
+
204
+ The FHIR server simulation used to support clients in demonstrating their ability to access
205
+ FHIR APIs using access tokens obtained using the SMART flows is very limited. Testers are currently
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