smart_app_launch_test_kit 0.6.0 → 0.6.1

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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/SMART_RunClientAgainstServer.json.erb +31 -0
  3. data/config/presets/SMART_RunServerAgainstClient.json.erb +42 -0
  4. data/lib/smart_app_launch/backend_services_authorization_group.rb +0 -2
  5. data/lib/smart_app_launch/backend_services_authorization_request_success_test.rb +5 -2
  6. data/lib/smart_app_launch/backend_services_authorization_response_body_test.rb +6 -2
  7. data/lib/smart_app_launch/client_stu2_2_suite.rb +79 -0
  8. data/lib/smart_app_launch/client_suite/client_access_group.rb +26 -0
  9. data/lib/smart_app_launch/client_suite/client_access_interaction_test.rb +64 -0
  10. data/lib/smart_app_launch/client_suite/client_registration_group.rb +15 -0
  11. data/lib/smart_app_launch/client_suite/client_registration_verification_test.rb +52 -0
  12. data/lib/smart_app_launch/client_suite/client_token_request_verification_test.rb +146 -0
  13. data/lib/smart_app_launch/client_suite/client_token_use_verification_test.rb +47 -0
  14. data/lib/smart_app_launch/docs/demo/FHIR Request.postman_collection.json +81 -0
  15. data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +121 -0
  16. data/lib/smart_app_launch/endpoints/echoing_fhir_responder.rb +52 -0
  17. data/lib/smart_app_launch/endpoints/mock_smart_server/token.rb +27 -0
  18. data/lib/smart_app_launch/endpoints/mock_smart_server.rb +217 -0
  19. data/lib/smart_app_launch/metadata.rb +2 -2
  20. data/lib/smart_app_launch/smart_stu2_2_suite.rb +2 -1
  21. data/lib/smart_app_launch/smart_stu2_suite.rb +2 -1
  22. data/lib/smart_app_launch/tags.rb +7 -0
  23. data/lib/smart_app_launch/token_introspection_response_group.rb +1 -1
  24. data/lib/smart_app_launch/urls.rb +40 -0
  25. data/lib/smart_app_launch/version.rb +2 -2
  26. data/lib/smart_app_launch_test_kit.rb +1 -0
  27. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbce79e8d195045070c2257dc897fa511b981071d366875bf3aa36ccb338dbdd
4
- data.tar.gz: 5ea51bb46c93c23186fa620908287f69bab62d42c8f5ecbf0ff70d39ec8523e0
3
+ metadata.gz: 2608337f4d0d2651ba2395ecda2f89263ebe14d039f3d730ae2f82df3fe49855
4
+ data.tar.gz: b7a480f55c94dec25865151faefd76045096d21b5333a9c5bf56cf73eb7310bd
5
5
  SHA512:
6
- metadata.gz: 7f6d08a9ea9be426cd26e67fcbfa96bc4051b83aa9d38837f77fda2f9715a84e766182993a8e1f725fa945ea9c0bbf68f32155827482d7100036e20f02fd9e97
7
- data.tar.gz: cba999b57e6317c865fc1b98dd56a042b98d2439af5ec8e28ffefd58d52e871513cd7152f640a9d449f1145a870b66ae1fd6d2343376cf0347eb52f8c2373c6d
6
+ metadata.gz: 853bd240f1fccdfd72ab79ff89b420b9b1edebd1d1fbd01e9dd3f1e206a29fae3f84f79371e0c7406dc9f3f5bde6ddf7ef51167cf2537f97b56bc36d245d7ef8
7
+ data.tar.gz: 5cf5840b0061bb6605e68e5d2ce4bf8e07c3039b4e53d8678b2d5cad19e27c352c751eadc8325dfd6ba3df02ddc28f9a7a8e5664eda85aa6cb77c93d1485b52a
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "Demo: Run Against the SMART Server Suite",
3
+ "id": "smart_run_client_against_server_v2_2",
4
+ "test_suite_id": "smart_client_stu2_2",
5
+ "inputs": [
6
+ {
7
+ "name": "smart_jwk_set",
8
+ "description": "The SMART client's JSON Web Key Set. May be provided as either a publicly accessible url containing the JWKS, or the raw JWKS.",
9
+ "optional": true,
10
+ "title": "SMART JSON Web Key Set (JWKS)",
11
+ "type": "textarea",
12
+ "value": "<%= Inferno::Application['base_url'] %>/custom/smart_stu2_2/.well-known/jwks.json"
13
+ },
14
+ {
15
+ "name": "client_id",
16
+ "description": "If a particular client id is desired, put it here. Otherwise a default of the Inferno session id will be used.",
17
+ "optional": true,
18
+ "title": "Client Id",
19
+ "type": "text",
20
+ "value": "smart_client_test_demo"
21
+ },
22
+ {
23
+ "name": "echoed_fhir_response",
24
+ "description": "JSON representation of a FHIR resource for Inferno to echo when a request is made to the simulated FHIR server. The provided content will be echoed back exactly and no check will be made that it is appropriate for the request made. If nothing is provided, an OperationOutcome will be returned.",
25
+ "optional": true,
26
+ "title": "FHIR Response to Echo",
27
+ "type": "textarea",
28
+ "value": "{\n \"resourceType\": \"Patient\",\n \"id\": \"example\",\n \"name\": [\n {\n \"family\": \"Chalmers\",\n \"given\": [\n \"Peter\",\n \"James\"\n ]\n }\n ],\n \"gender\": \"male\",\n \"birthDate\": \"1974-12-25\",\n \"address\": [\n {\n \"line\": [\n \"534 Erewhon St\"\n ],\n \"city\": \"Ann Arbor\",\n \"state\": \"MI\",\n \"postalCode\": \"48108\"\n }\n ]\n}"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "title": "Demo: Run Against the SMART Client Suite",
3
+ "id": "smart_run_server_against_client_v2_2",
4
+ "test_suite_id": "smart_stu2_2",
5
+ "inputs": [
6
+ {
7
+ "name": "url",
8
+ "description": "URL of the FHIR endpoint used by SMART applications",
9
+ "title": "FHIR Endpoint",
10
+ "type": "text",
11
+ "value": "<%= Inferno::Application['base_url'] %>/custom/smart_client_stu2_2/fhir"
12
+ },
13
+ {
14
+ "name": "backend_services_smart_auth_info",
15
+ "options": {
16
+ "mode": "auth",
17
+ "components": [
18
+ {
19
+ "name": "auth_type",
20
+ "default": "backend_services",
21
+ "locked": "true"
22
+ },
23
+ {
24
+ "name": "use_discovery",
25
+ "locked": true
26
+ }
27
+ ]
28
+ },
29
+ "title": "Backend Services Credentials",
30
+ "type": "auth_info",
31
+ "value": {
32
+ "encryption_algorithm": "ES384",
33
+ "auth_type": "backend_services",
34
+ "use_discovery": "true",
35
+ "token_url": "<%= Inferno::Application['base_url'] %>/custom/smart_client_stu2_2/auth/token",
36
+ "requested_scopes": "system/*.rs",
37
+ "client_id": "smart_client_test_demo"
38
+ },
39
+ "default": {}
40
+ }
41
+ ]
42
+ }
@@ -31,8 +31,6 @@ module SMARTAppLaunch
31
31
  ]
32
32
  }
33
33
 
34
- output :bearer_token
35
-
36
34
  test from: :smart_tls,
37
35
  id: :smart_backend_services_token_tls_version,
38
36
  title: 'Authorization service token endpoint secured by transport layer security',
@@ -23,7 +23,7 @@ module SMARTAppLaunch
23
23
  ]
24
24
  }
25
25
 
26
- output :authentication_response
26
+ output :authentication_response, :smart_auth_info
27
27
 
28
28
  run do
29
29
  post_request_content = BackendServicesAuthorizationRequestBuilder.build(
@@ -40,7 +40,10 @@ module SMARTAppLaunch
40
40
 
41
41
  assert_response_status([200, 201])
42
42
 
43
- output authentication_response: authentication_response.response_body
43
+ smart_auth_info.issue_time = Time.now
44
+
45
+ output authentication_response: authentication_response.response_body,
46
+ smart_auth_info: smart_auth_info
44
47
  end
45
48
  end
46
49
  end
@@ -29,7 +29,7 @@ module SMARTAppLaunch
29
29
  }
30
30
  ]
31
31
  }
32
- output :bearer_token, :smart_auth_info
32
+ output :bearer_token, :smart_auth_info, :received_scopes
33
33
 
34
34
  run do
35
35
  skip_if authentication_response.blank?, 'No authentication response received.'
@@ -38,11 +38,15 @@ module SMARTAppLaunch
38
38
  response_body = JSON.parse(authentication_response)
39
39
 
40
40
  access_token = response_body['access_token']
41
+ received_scopes = response_body['scope']
42
+ expires_in = response_body['expires_in']
43
+
41
44
  assert access_token.present?, 'Token response did not contain access_token as required'
42
45
 
43
46
  smart_auth_info.access_token = access_token
47
+ smart_auth_info.expires_in = expires_in
44
48
 
45
- output bearer_token: access_token, smart_auth_info: smart_auth_info
49
+ output bearer_token: access_token, smart_auth_info: smart_auth_info, received_scopes: received_scopes
46
50
 
47
51
  required_keys = ['token_type', 'expires_in', 'scope']
48
52
 
@@ -0,0 +1,79 @@
1
+ require_relative 'endpoints/mock_smart_server/token'
2
+ require_relative 'endpoints/echoing_fhir_responder'
3
+ require_relative 'urls'
4
+ require_relative 'client_suite/client_registration_group'
5
+ require_relative 'client_suite/client_access_group'
6
+
7
+ module SMARTAppLaunch
8
+ class SMARTClientSTU22Suite < Inferno::TestSuite
9
+ id :smart_client_stu2_2 # rubocop:disable Naming/VariableNumber
10
+ title 'SMART App Launch STU2.2 Client'
11
+ description File.read(File.join(__dir__, 'docs', 'smart_stu2_2_client_suite_description.md'))
12
+
13
+ links [
14
+ {
15
+ type: 'source_code',
16
+ label: 'Open Source',
17
+ url: 'https://github.com/inferno-framework/smart-app-launch-test-kit/'
18
+ },
19
+ {
20
+ type: 'report_issue',
21
+ label: 'Report Issue',
22
+ url: 'https://github.com/inferno-framework/smart-app-launch-test-kit/issues/'
23
+ },
24
+ {
25
+ type: 'download',
26
+ label: 'Download',
27
+ url: 'https://github.com/inferno-framework/smart-app-launch-test-kit/releases/'
28
+ },
29
+ {
30
+ type: 'ig',
31
+ label: 'Implementation Guide',
32
+ url: 'https://hl7.org/fhir/smart-app-launch/STU2.2/'
33
+ }
34
+ ]
35
+
36
+ route(:get, SMART_DISCOVERY_PATH, ->(_env) {MockSMARTServer.smart_server_metadata(id) })
37
+ suite_endpoint :post, TOKEN_PATH, MockSMARTServer::TokenEndpoint
38
+ suite_endpoint :get, FHIR_PATH, EchoingFHIRResponderEndpoint
39
+ suite_endpoint :post, FHIR_PATH, EchoingFHIRResponderEndpoint
40
+ suite_endpoint :put, FHIR_PATH, EchoingFHIRResponderEndpoint
41
+ suite_endpoint :delete, FHIR_PATH, EchoingFHIRResponderEndpoint
42
+ suite_endpoint :get, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
43
+ suite_endpoint :post, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
44
+ suite_endpoint :put, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
45
+ suite_endpoint :delete, "#{FHIR_PATH}/:one", EchoingFHIRResponderEndpoint
46
+ suite_endpoint :get, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
47
+ suite_endpoint :post, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
48
+ suite_endpoint :put, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
49
+ suite_endpoint :delete, "#{FHIR_PATH}/:one/:two", EchoingFHIRResponderEndpoint
50
+ suite_endpoint :get, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
51
+ suite_endpoint :post, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
52
+ suite_endpoint :put, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
53
+ suite_endpoint :delete, "#{FHIR_PATH}/:one/:two/:three", EchoingFHIRResponderEndpoint
54
+
55
+ resume_test_route :get, RESUME_PASS_PATH do |request|
56
+ request.query_parameters['token']
57
+ end
58
+
59
+ resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
60
+ request.query_parameters['token']
61
+ end
62
+
63
+ group do
64
+ title 'SMART Backend Services'
65
+ description %(
66
+ During these tests, the client will use SMART Backend Services
67
+ to access a FHIR API. Clients will provide registeration details,
68
+ obtain an access token, and use the access token when making a
69
+ request to a FHIR API.
70
+ )
71
+
72
+ input :smart_jwk_set,
73
+ optional: false
74
+
75
+ group from: :smart_client_registration
76
+ group from: :smart_client_access
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'client_access_interaction_test'
2
+ require_relative 'client_token_request_verification_test'
3
+ require_relative 'client_token_use_verification_test'
4
+
5
+ module SMARTAppLaunch
6
+ class SMARTClientAccess < Inferno::TestGroup
7
+ id :smart_client_access
8
+ title 'Client Access'
9
+ description %(
10
+ During these tests, the client system will access Inferno's simulated
11
+ FHIR server by requesting an access token and making a FHIR request.
12
+ Inferno will then verify that any token requests made were conformant
13
+ and that a token returned from a token request was used on an access request.
14
+ )
15
+
16
+ run_as_group
17
+
18
+ input :smart_jwk_set,
19
+ optional: true,
20
+ locked: true
21
+
22
+ test from: :smart_client_access_interaction
23
+ test from: :smart_client_token_request_verification
24
+ test from: :smart_client_token_use_verification
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ require_relative '../urls'
2
+ require_relative '../endpoints/mock_smart_server'
3
+
4
+ module SMARTAppLaunch
5
+ class SMARTClientAccessInteraction < Inferno::Test
6
+ include URLs
7
+
8
+ id :smart_client_access_interaction
9
+ title 'Perform SMART-secured Access'
10
+ description %(
11
+ During this test, Inferno will wait for the client to access data
12
+ using a SMART token obtained during earlier tests.
13
+ )
14
+ input :client_id,
15
+ title: 'Client Id',
16
+ type: 'text',
17
+ locked: true,
18
+ description: %(
19
+ The registered Client Id for use in obtaining access tokens.
20
+ Create a new session if you need to change this value.
21
+ )
22
+ input :smart_jwk_set,
23
+ title: 'JSON Web Key Set (JWKS)',
24
+ type: 'textarea',
25
+ optional: true,
26
+ locked: true,
27
+ description: %(
28
+ The SMART client's JSON Web Key Set in the form of either a publicly accessible url
29
+ containing the JWKS, or the raw JWKS JSON. Must include the key(s) Inferno will need to
30
+ verify signatures on token requests made by the client.
31
+ Create a new session if you need to change this value.
32
+ )
33
+ input :echoed_fhir_response,
34
+ title: 'FHIR Response to Echo',
35
+ type: 'textarea',
36
+ description: %(
37
+ JSON representation of a FHIR resource for Inferno to echo when a request
38
+ is made to the simulated FHIR server. The provided content will be echoed
39
+ back exactly and no check will be made that it is appropriate for the request
40
+ made. If nothing is provided, an OperationOutcome will be returned.
41
+ ),
42
+ optional: true
43
+
44
+ run do
45
+ wait(
46
+ identifier: client_id,
47
+ message: %(
48
+ **Access**
49
+
50
+ Use the registered client id (#{client_id}) to obtain an access
51
+ token using SMART Backend Services
52
+ and use that token to access a FHIR endpoint under the simulated server's base URL
53
+
54
+ `#{client_fhir_base_url}`
55
+
56
+ Inferno will echo the response provided in the **FHIR Response to Echo** input.
57
+
58
+ [Click here](#{client_resume_pass_url}?token=#{client_id}) once you performed
59
+ the access.
60
+ )
61
+ )
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'client_registration_verification_test'
2
+
3
+ module SMARTAppLaunch
4
+ class SMARTClientRegistration < Inferno::TestGroup
5
+ id :smart_client_registration
6
+ title 'Client Registration'
7
+ description %(
8
+ During these tests, Inferno will verify the registration details provided as inputs,
9
+ including the client's JSON Web Key Set.
10
+ )
11
+ run_as_group
12
+
13
+ test from: :smart_client_registration_verification
14
+ end
15
+ end
@@ -0,0 +1,52 @@
1
+ require_relative '../tags'
2
+ require_relative '../endpoints/mock_smart_server'
3
+
4
+ module SMARTAppLaunch
5
+ class SMARTClientRegistrationVerification < Inferno::Test
6
+
7
+ id :smart_client_registration_verification
8
+ title 'Verify SMART Registration'
9
+ description %(
10
+ During this test, Inferno will verify that the SMART registration details
11
+ provided are conformant.
12
+ )
13
+ input :smart_jwk_set,
14
+ title: 'SMART JSON Web Key Set (JWKS)',
15
+ type: 'textarea',
16
+ description: %(
17
+ The SMART client's JSON Web Key Set including the key(s) Inferno will need to
18
+ verify signatures on token requests made by the client. May be provided as either
19
+ a publicly accessible url containing the JWKS, or the raw JWKS JSON.
20
+ )
21
+ input :client_id,
22
+ title: 'Client Id',
23
+ type: 'text',
24
+ optional: true,
25
+ description: %(
26
+ If a particular client id is desired, put it here. Otherwise a
27
+ default of the Inferno session id will be used.
28
+ )
29
+
30
+ output :client_id
31
+
32
+ run do
33
+ omit_if smart_jwk_set.blank?, # for re-use: mark the smart_jwk_set input as optional when importing to enable
34
+ 'Not configured for SMART authentication.'
35
+
36
+ if client_id.blank?
37
+ client_id = test_session_id
38
+ output(client_id:)
39
+ end
40
+
41
+ jwks_warnings = []
42
+ parsed_smart_jwk_set = MockSMARTServer.jwk_set(smart_jwk_set, jwks_warnings)
43
+ jwks_warnings.each { |warning| add_message('warning', warning) }
44
+
45
+ assert parsed_smart_jwk_set.length.positive?, 'JWKS content does not include any valid keys.'
46
+
47
+ # TODO: add key-specific verification per end of https://build.fhir.org/ig/HL7/smart-app-launch/client-confidential-asymmetric.html#registering-a-client-communicating-public-keys
48
+
49
+ assert messages.none? { |msg| msg[:type] == 'error' }, 'Invalid key set provided. See messages for details'
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,146 @@
1
+ require_relative '../tags'
2
+ require_relative '../urls'
3
+ require_relative '../endpoints/mock_smart_server'
4
+
5
+ module SMARTAppLaunch
6
+ class SMARTClientTokenRequestVerification < Inferno::Test
7
+ include URLs
8
+
9
+ id :smart_client_token_request_verification
10
+ title 'Verify SMART Token Requests'
11
+ description %(
12
+ Check that SMART token requests are conformant.
13
+ )
14
+
15
+ input :client_id,
16
+ title: 'Client Id',
17
+ type: 'text',
18
+ optional: false,
19
+ locked: true,
20
+ description: %(
21
+ The registered Client Id for use in obtaining access tokens.
22
+ Create a new session if you need to change this value.
23
+ )
24
+ input :smart_jwk_set,
25
+ title: 'JSON Web Key Set (JWKS)',
26
+ type: 'textarea',
27
+ optional: false,
28
+ locked: true,
29
+ description: %(
30
+ The SMART client's JSON Web Key Set in the form of either a publicly accessible url
31
+ containing the JWKS, or the raw JWKS JSON. Must include the key(s) Inferno will need to
32
+ verify signatures on token requests made by the client.
33
+ Create a new session if you need to change this value.
34
+ )
35
+ output :smart_tokens
36
+
37
+ run do
38
+ omit_if smart_jwk_set.blank?, # for re-use: mark the smart_jwk_set input as optional when importing to enable
39
+ 'SMART Backend Services authentication not demonstrated as a part of this test session.'
40
+
41
+ load_tagged_requests(TOKEN_TAG, SMART_TAG)
42
+ skip_if requests.blank?, 'No SMART token requests made.'
43
+
44
+ jti_list = []
45
+ token_list = []
46
+ requests.each_with_index do |token_request, index|
47
+ request_params = URI.decode_www_form(token_request.request_body).to_h
48
+ check_request_params(request_params, index + 1)
49
+ check_client_assertion(request_params['client_assertion'], index + 1, jti_list)
50
+ token_list << extract_token_from_response(token_request)
51
+ end
52
+
53
+ output smart_tokens: token_list.compact.join("\n")
54
+
55
+ assert messages.none? { |msg|
56
+ msg[:type] == 'error'
57
+ }, 'Invalid token requests detected. See messages for details.'
58
+ end
59
+
60
+ def check_request_params(params, request_num)
61
+ if params['grant_type'] != 'client_credentials'
62
+ add_message('error',
63
+ "Token request #{request_num} had an incorrect `grant_type`: expected 'client_credentials', " \
64
+ "but got '#{params['grant_type']}'")
65
+ end
66
+ if params['client_assertion_type'] != 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
67
+ add_message('error',
68
+ "Token request #{request_num} had an incorrect `client_assertion_type`: " \
69
+ "expected 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', " \
70
+ "but got '#{params['client_assertion_type']}'")
71
+ end
72
+ return unless params['scope'].blank?
73
+
74
+ add_message('error', "Token request #{request_num} did not include the requested `scope`")
75
+ end
76
+
77
+ def check_client_assertion(assertion, request_num, jti_list)
78
+ decoded_token =
79
+ begin
80
+ JWT::EncodedToken.new(assertion)
81
+ rescue StandardError => e
82
+ add_message('error', "Token request #{request_num} contained an invalid client assertion jwt: #{e}")
83
+ nil
84
+ end
85
+
86
+ return unless decoded_token.present?
87
+
88
+ check_jwt_header(decoded_token.header, request_num)
89
+ check_jwt_payload(decoded_token.payload, request_num, jti_list)
90
+ check_jwt_signature(decoded_token, request_num)
91
+ end
92
+
93
+ def check_jwt_header(header, request_num)
94
+ return unless header['typ'] != 'JWT'
95
+
96
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `typ` header: " \
97
+ "expected 'JWT', got '#{header['typ']}'")
98
+ end
99
+
100
+ def check_jwt_payload(claims, request_num, jti_list)
101
+ if claims['iss'] != client_id
102
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `iss` claim: " \
103
+ "expected '#{client_id}', got '#{claims['iss']}'")
104
+ end
105
+
106
+ if claims['sub'] != client_id
107
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `sub` claim: " \
108
+ "expected '#{client_id}', got '#{claims['sub']}'")
109
+ end
110
+
111
+ if claims['aud'] != client_token_url
112
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `aud` claim: " \
113
+ "expected '#{client_token_url}', got '#{claims['aud']}'")
114
+ end
115
+
116
+ if claims['exp'].blank?
117
+ add_message('error', "client assertion jwt on token request #{request_num} is missing the `exp` claim.")
118
+ end
119
+
120
+ if claims['jti'].blank?
121
+ add_message('error', "client assertion jwt on token request #{request_num} is missing the `jti` claim.")
122
+ elsif jti_list.include?(claims['jti'])
123
+ add_message('error', "client assertion jwt on token request #{request_num} has a `jti` claim that was " \
124
+ "previouly used: '#{claims['jti']}'.")
125
+ else
126
+ jti_list << claims['jti']
127
+ end
128
+ end
129
+
130
+ def check_jwt_signature(encoded_token, request_num)
131
+ error = MockSMARTServer.smart_assertion_signature_verification(encoded_token, smart_jwk_set)
132
+
133
+ return unless error.present?
134
+
135
+ add_message('error', "Signature validation failed on token request #{request_num}: #{error}")
136
+ end
137
+
138
+ def extract_token_from_response(request)
139
+ return unless request.status == 200
140
+
141
+ JSON.parse(request.response_body)&.dig('access_token')
142
+ rescue
143
+ nil
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,47 @@
1
+ require_relative '../tags'
2
+ require_relative '../endpoints/mock_smart_server'
3
+
4
+ module SMARTAppLaunch
5
+ class SMARTClientTokenUseVerification < Inferno::Test
6
+
7
+ id :smart_client_token_use_verification
8
+ title 'Verify SMART Token Use'
9
+ description %(
10
+ Check that a SMART token returned to the client was used for request
11
+ authentication.
12
+ )
13
+
14
+ input :smart_tokens,
15
+ 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
+
20
+ def access_request_tags
21
+ return config.options[:access_request_tags] if config.options[:access_request_tags].present?
22
+
23
+ [ACCESS_TAG]
24
+ end
25
+
26
+ 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
+ access_requests = access_request_tags.map do |access_request_tag|
31
+ load_tagged_requests(access_request_tag).reject { |access| access.status == 401 }
32
+ end.flatten
33
+ obtained_tokens = smart_tokens&.split("\n")
34
+
35
+ skip_if obtained_tokens.blank?, 'No token requests made.'
36
+ skip_if access_requests.blank?, 'No successful access requests made.'
37
+
38
+ used_tokens = access_requests.map do |access_request|
39
+ access_request.request_headers.find do |header|
40
+ header.name.downcase == 'authorization'
41
+ end&.value&.delete_prefix('Bearer ')
42
+ end.compact
43
+
44
+ assert (used_tokens & obtained_tokens).present?, 'Returned tokens never used in any requests.'
45
+ end
46
+ end
47
+ 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,121 @@
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 [SMART App Launch IG](http://hl7.org/fhir/smart-app-launch/STU2.2/)
11
+ for authorizating and/or authenticating with a server in order to gain
12
+ access to HL7® FHIR® APIs. At this time, the suite only contains tests for
13
+ the [Backend Services](https://hl7.org/fhir/smart-app-launch/STU2.2/backend-services.html)
14
+ flow.
15
+
16
+ These tests are a **DRAFT** intended to allow implementers to perform
17
+ preliminary checks of their systems against SMART requirements and
18
+ [provide feedback](https://github.com/inferno-framework/smart-app-launch-test-kit/issues)
19
+ on the tests. Future versions of these tests may verify other
20
+ requirements and may change the test verification logic.
21
+
22
+ ## Test Methodology
23
+
24
+ For these tests Inferno simulates a SMART server that supports the backend services
25
+ flow. Testers will
26
+ 1. Provide registration details as inputs, including a JSON Web Key Set (JWKS)
27
+ an optionally a client id if a specific one should be used.
28
+ 2. Request an access token using the registered JWKS and client id.
29
+ 3. Use that access token on a FHIR API request.
30
+
31
+ The simulated server is relatively permissive in the sense that it will often
32
+ provide successful responses even when the request is not conformant. When
33
+ requesting tokens, Inferno will return an access token as long as it can find
34
+ the client id and the signature is valid. This allows incomplete systems to
35
+ run the tests. However, these non-conformant requests will be flagged by
36
+ the tests as failures so that systems will not pass the tests without being
37
+ fully conformant.
38
+
39
+ ## Running the Tests
40
+
41
+ ### Quick Start
42
+
43
+ The following inputs must be provided by the tester at a minimum to execute
44
+ any tests in this suite:
45
+ 1. **SMART JSON Web Key Set (JWKS)**: The SMART client's public JSON Web Key Set including
46
+ key(s) that Inferno will use to verify the signature on incoming token requests. May
47
+ be provided as either a publicly accessible url containing the JWKS, or the raw JWKS.
48
+
49
+ The *Additional Inputs* section below describes options available to customize
50
+ the behavior of Inferno's server simulation.
51
+
52
+ ### Demonstration
53
+
54
+ To try out these tests without a SMART client implementation, these tests can be exercised
55
+ using the SMART App Launch server test suite and a simple HTTP request generator. The following
56
+ steps use [Postman](https://www.postman.com/) to generate the access request using
57
+ [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
+ steps.
59
+
60
+ 1. Start an instance of the SMART App Launch STU2.2 Client test suite.
61
+ 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"
65
+ 6. Select test group **3** Backend Services from the left panel, click the "RUN TESTS" button
66
+ in the upper right, and click "SUBMIT"
67
+ 7. Find the access token to use for the data access request by opening test **3.2.05** Authorization
68
+ request succeeds when supplied correct information, click on the "REQUESTS" tab, clicking on the "DETAILS"
69
+ 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)
71
+ 8. Open Postman and open the "FHIR Request" Collection. Click the "Variables" tab and add the copied access token
72
+ as the current value of the `bearer_token` variable. Also update the
73
+ `base_url` value for where the test is running (see details on the "Overview" tab).
74
+ Save the collection.
75
+ 9. Select the "Patient Read" request and click "Send". A FHIR Patient resource should be returned.
76
+ 10. Return to the client tests and click the link to continue and complete the tests.
77
+
78
+ The client tests should pass with the exception of test **1.2.02** Verify SMART Token Requests. This is
79
+ expected as the Server tests make several intentionally invalid token requests. Inferno's simulated SMART
80
+ server responds successfully to those requests when the client id can be identified, but flags them as
81
+ not conformant causing these expected failures. Because responding with an access token to non-conformant
82
+ token requests is itself not conformant there are corresponding failures on the server test in tests **3.2.02**,
83
+ **3.2.04**, and **3.2.04**. There may be other SMART server test failures due to an assumption that
84
+ servers support the app launch capabilities in addition to backend services.
85
+
86
+ ### Additional Inputs
87
+
88
+ Two additional inputs are available to support testers
89
+ - **Client Id**: Testers may specify a client id for Inferno to use for the test session if they
90
+ have one already configured.
91
+ - **FHIR Response to Echo**: The focus of this test kit is on the auth protocol, so the
92
+ simulated FHIR server implemented in this test suite is very simple and will by default
93
+ return a FHIR OperationOutcome to any request made. Testers may provide a static
94
+ FHIR JSON body for Inferno to return instead. In this case, the simulation is a simple
95
+ echo and Inferno does not check that the response if appropriate for the request made.
96
+
97
+ ## Current Limitations
98
+
99
+ 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. Notably, only the backend services flow
101
+ is tested at this time.
102
+
103
+ The following sections list other known gaps and limitations.
104
+
105
+ ### SMART Server Simulation Limitations
106
+
107
+ This test suite contains a simulation of a SMART Backend Services server which is not fully
108
+ general and not all conformant clients may be able to interact with it. However, the intention
109
+ is not to prevent systems from passing for making conformant choices that Inferno's simulation
110
+ does not support. One specific example is that the SMART configuration metadata available at
111
+ `.well-known/smart-configuration` for the simulated server is fixed and cannot be changed by
112
+ testers at this time. Please report any issues that prevent conformant systems from passing in
113
+ the [github repository's issues page](https://github.com/inferno-framework/smart-app-launch-test-kit/issues/).
114
+
115
+ ### FHIR Server Simulation Limitations
116
+
117
+ The FHIR server simulation used to support clients in demonstrating their ability to access
118
+ 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 FHIR request made. While
120
+ Inferno will never implement a fully general FHIR server simulation, additional options may be added
121
+ in the future based on community feedback.
@@ -0,0 +1,52 @@
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.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
+
18
+ # If the tester provided a response, echo it
19
+ # otherwise, operation outcome
20
+ echo_response = JSON.parse(result.input_json)
21
+ .find { |input| input['name'].include?('echoed_fhir_response') }
22
+ &.dig('value')
23
+
24
+ unless echo_response.present?
25
+ response.status = 400
26
+ response.body = FHIR::OperationOutcome.new(
27
+ issue: FHIR::OperationOutcome::Issue.new(
28
+ severity: 'fatal', code: 'required',
29
+ details: FHIR::CodeableConcept.new(text: 'No response provided to echo.')
30
+ )
31
+ ).to_json
32
+ return
33
+ end
34
+
35
+ response.status = 200
36
+ response.body = echo_response
37
+ end
38
+
39
+ def update_result
40
+ if MockSMARTServer.request_has_expired_token?(request)
41
+ MockSMARTServer.update_response_for_expired_token(response)
42
+ return
43
+ end
44
+
45
+ nil # never update for now
46
+ end
47
+
48
+ def tags
49
+ [ACCESS_TAG]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
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
+ module MockSMARTServer
9
+ class TokenEndpoint < Inferno::DSL::SuiteEndpoint
10
+ def test_run_identifier
11
+ MockSMARTServer.client_id_from_client_assertion(request.params[:client_assertion])
12
+ end
13
+
14
+ def make_response
15
+ MockSMARTServer.make_smart_token_response(request, response, result)
16
+ end
17
+
18
+ def update_result
19
+ nil # never update for now
20
+ end
21
+
22
+ def tags
23
+ [TOKEN_TAG, SMART_TAG]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,217 @@
1
+ require 'jwt'
2
+ require 'faraday'
3
+ require 'time'
4
+ require_relative '../urls'
5
+ require_relative '../tags'
6
+
7
+ module SMARTAppLaunch
8
+ module MockSMARTServer
9
+ SUPPORTED_SCOPES = ['openid', 'system/*.read', 'user/*.read', 'patient/*.read'].freeze
10
+
11
+ module_function
12
+
13
+ def smart_server_metadata(suite_id)
14
+ base_url = "#{Inferno::Application['base_url']}/custom/#{suite_id}"
15
+ response_body = {
16
+ token_endpoint_auth_signing_alg_values_supported: ['RS384', 'ES384'],
17
+ capabilities: ['client-confidential-asymmetric'],
18
+ code_challenge_methods_supported: ['S256'],
19
+ token_endpoint_auth_methods_supported: ['private_key_jwt'],
20
+ issuer: base_url + FHIR_PATH,
21
+ grant_types_supported: ['client_credentials'],
22
+ scopes_supported: SUPPORTED_SCOPES,
23
+ token_endpoint: base_url + TOKEN_PATH
24
+ }.to_json
25
+
26
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
27
+ end
28
+
29
+ def make_smart_token_response(request, response, result)
30
+ assertion = request.params[:client_assertion]
31
+ client_id = client_id_from_client_assertion(assertion)
32
+
33
+ key_set_input = JSON.parse(result.input_json)&.find do |input|
34
+ input['name'] == 'smart_jwk_set'
35
+ end&.dig('value')
36
+ signature_error = smart_assertion_signature_verification(assertion, key_set_input)
37
+
38
+ if signature_error.present?
39
+ update_response_for_invalid_assertion(response, signature_error)
40
+ return
41
+ end
42
+
43
+ exp_min = 60
44
+ response_body = {
45
+ access_token: client_id_to_token(client_id, exp_min),
46
+ token_type: 'Bearer',
47
+ expires_in: 60 * exp_min,
48
+ scope: request.params[:scope]
49
+ }
50
+
51
+ response.body = response_body.to_json
52
+ response.headers['Cache-Control'] = 'no-store'
53
+ response.headers['Pragma'] = 'no-cache'
54
+ response.headers['Access-Control-Allow-Origin'] = '*'
55
+ response.content_type = 'application/json'
56
+ response.status = 200
57
+ end
58
+
59
+ def client_id_from_client_assertion(client_assertion_jwt)
60
+ return unless client_assertion_jwt.present?
61
+
62
+ jwt_claims(client_assertion_jwt)&.dig('iss')
63
+ end
64
+
65
+ def parsed_request_body(request)
66
+ JSON.parse(request.request_body)
67
+ rescue JSON::ParserError
68
+ nil
69
+ end
70
+
71
+ def parsed_io_body(request)
72
+ parsed_body = begin
73
+ JSON.parse(request.body.read)
74
+ rescue JSON::ParserError
75
+ nil
76
+ end
77
+ request.body.rewind
78
+
79
+ parsed_body
80
+ end
81
+
82
+ def jwt_claims(encoded_jwt)
83
+ JWT.decode(encoded_jwt, nil, false)[0]
84
+ end
85
+
86
+ def client_uri_to_client_id(client_uri)
87
+ Base64.urlsafe_encode64(client_uri, padding: false)
88
+ end
89
+
90
+ def client_id_to_client_uri(client_id)
91
+ Base64.urlsafe_decode64(client_id)
92
+ end
93
+
94
+ def client_id_to_token(client_id, exp_min)
95
+ token_structure = {
96
+ client_id:,
97
+ expiration: exp_min.minutes.from_now.to_i,
98
+ nonce: SecureRandom.hex(8)
99
+ }.to_json
100
+
101
+ Base64.urlsafe_encode64(token_structure, padding: false)
102
+ end
103
+
104
+ def decode_token(token)
105
+ JSON.parse(Base64.urlsafe_decode64(token))
106
+ rescue JSON::ParserError
107
+ nil
108
+ end
109
+
110
+ def token_to_client_id(token)
111
+ decode_token(token)&.dig('client_id')
112
+ end
113
+
114
+ def jwk_set(jku, warning_messages = []) # rubocop:disable Metrics/CyclomaticComplexity
115
+ jwk_set = JWT::JWK::Set.new
116
+
117
+ if jku.blank?
118
+ warning_messages << 'No key set input.'
119
+ return jwk_set
120
+ end
121
+
122
+ jwk_body = # try as raw jwk set
123
+ begin
124
+ JSON.parse(jku)
125
+ rescue JSON::ParserError
126
+ nil
127
+ end
128
+
129
+ if jwk_body.blank?
130
+ retrieved = Faraday.get(jku) # try as url pointing to a jwk set
131
+ jwk_body =
132
+ begin
133
+ JSON.parse(retrieved.body)
134
+ rescue JSON::ParserError
135
+ warning_messages << "Failed to fetch valid json from jwks uri #{jwk_set}."
136
+ nil
137
+ end
138
+ else
139
+ warning_messages << 'Providing the JWK Set directly is strongly discouraged.'
140
+ end
141
+
142
+ return jwk_set if jwk_body.blank?
143
+
144
+ jwk_body['keys']&.each_with_index do |key_hash, index|
145
+ parsed_key =
146
+ begin
147
+ JWT::JWK.new(key_hash)
148
+ rescue JWT::JWKError => e
149
+ id = key_hash['kid'] | index
150
+ warning_messages << "Key #{id} invalid: #{e}"
151
+ nil
152
+ end
153
+ jwk_set << parsed_key unless parsed_key.blank?
154
+ end
155
+
156
+ jwk_set
157
+ end
158
+
159
+ def request_has_expired_token?(request)
160
+ return false if request.params[:session_path].present?
161
+
162
+ token = request.headers['authorization']&.delete_prefix('Bearer ')
163
+ decoded_token = decode_token(token)
164
+ return false unless decoded_token&.dig('expiration').present?
165
+
166
+ decoded_token['expiration'] < Time.now.to_i
167
+ end
168
+
169
+ def update_response_for_expired_token(response)
170
+ response.status = 401
171
+ response.format = :json
172
+ response.body = FHIR::OperationOutcome.new(
173
+ issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'expired',
174
+ details: FHIR::CodeableConcept.new(text: 'Bearer token has expired'))
175
+ ).to_json
176
+ end
177
+
178
+ def smart_assertion_signature_verification(token, key_set_input) # rubocop:disable Metrics/CyclomaticComplexity
179
+ encoded_token = nil
180
+ if token.is_a?(JWT::EncodedToken)
181
+ encoded_token = token
182
+ else
183
+ begin
184
+ encoded_token = JWT::EncodedToken.new(token)
185
+ rescue StandardError => e
186
+ return "invalid token structure: #{e}"
187
+ end
188
+ end
189
+ return 'invalid token' unless encoded_token.present?
190
+ return 'missing `alg` header' if encoded_token.header['alg'].blank?
191
+ return 'missing `kid` header' if encoded_token.header['kid'].blank?
192
+
193
+ jwk = identify_smart_signing_key(encoded_token.header['kid'], encoded_token.header['jku'], key_set_input)
194
+ return "no key found with `kid` '#{encoded_token.header['kid']}'" if jwk.blank?
195
+
196
+ begin
197
+ encoded_token.verify_signature!(algorithm: encoded_token.header['alg'], key: jwk.verify_key)
198
+ rescue StandardError => e
199
+ return e
200
+ end
201
+
202
+ nil
203
+ end
204
+
205
+ def identify_smart_signing_key(kid, jku, key_set_input)
206
+ key_set = jku.present? ? jku : key_set_input
207
+ parsed_key_set = jwk_set(key_set)
208
+ parsed_key_set&.find { |key| key.kid == kid }
209
+ end
210
+
211
+ def update_response_for_invalid_assertion(response, error_message)
212
+ response.status = 401
213
+ response.format = :json
214
+ response.body = { error: 'invalid_client', error_description: error_message }.to_json
215
+ end
216
+ end
217
+ end
@@ -64,12 +64,12 @@ module SMARTAppLaunch
64
64
  section](https://github.com/inferno-framework/smart-app-launch-test-kit/issues)
65
65
  of the repository.
66
66
  DESCRIPTION
67
- suite_ids [:smart, :smart_stu2, :smart_stu2_2, :smart_access_brands]
67
+ suite_ids [:smart, :smart_stu2, :smart_stu2_2, :smart_access_brands, :smart_client_stu2_2]
68
68
  tags ['SMART App Launch', 'Endpoint Publication']
69
69
  last_updated LAST_UPDATED
70
70
  version VERSION
71
71
  maturity 'Medium'
72
- authors ['Stephen MacVicar']
72
+ authors ['Stephen MacVicar', 'Karl Naden']
73
73
  repo 'https://github.com/inferno-framework/smart-app-launch-test-kit'
74
74
  end
75
75
  end
@@ -262,7 +262,8 @@ module SMARTAppLaunch
262
262
  smart_auth_info: { name: :backend_services_smart_auth_info }
263
263
  },
264
264
  outputs: {
265
- smart_auth_info: { name: :backend_services_smart_auth_info }
265
+ smart_auth_info: { name: :backend_services_smart_auth_info },
266
+ received_scopes: { name: :backend_services_received_scopes }
266
267
  }
267
268
  }
268
269
  end
@@ -260,7 +260,8 @@ module SMARTAppLaunch
260
260
  smart_auth_info: { name: :backend_services_smart_auth_info }
261
261
  },
262
262
  outputs: {
263
- smart_auth_info: { name: :backend_services_smart_auth_info }
263
+ smart_auth_info: { name: :backend_services_smart_auth_info },
264
+ received_scopes: { name: :backend_services_received_scopes }
264
265
  }
265
266
  }
266
267
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMARTAppLaunch
4
+ TOKEN_TAG = 'token'
5
+ SMART_TAG = 'smart'
6
+ ACCESS_TAG = 'access'
7
+ end
@@ -53,7 +53,7 @@ module SMARTAppLaunch
53
53
  introspection response and should match the claim in the ID token
54
54
  )
55
55
 
56
- input :standalone_smart_auth_info, type: :auth_info, options: { mode: 'auth' }
56
+ input :standalone_smart_auth_info, type: :auth_info, options: { mode: 'access' }
57
57
 
58
58
  input :standalone_received_scopes,
59
59
  title: 'Expected Introspection Response Value: scope',
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMARTAppLaunch
4
+ FHIR_PATH = '/fhir'
5
+ RESUME_PASS_PATH = '/resume_pass'
6
+ RESUME_FAIL_PATH = '/resume_fail'
7
+ AUTH_SERVER_PATH = '/auth'
8
+ SMART_DISCOVERY_PATH = "#{FHIR_PATH}/.well-known/smart-configuration".freeze
9
+ TOKEN_PATH = "#{AUTH_SERVER_PATH}/token".freeze
10
+
11
+ module URLs
12
+ def client_base_url
13
+ @client_base_url ||= "#{Inferno::Application['base_url']}/custom/#{client_suite_id}"
14
+ end
15
+
16
+ def client_fhir_base_url
17
+ @client_fhir_base_url ||= client_base_url + FHIR_PATH
18
+ end
19
+
20
+ def client_resume_pass_url
21
+ @client_resume_pass_url ||= client_base_url + RESUME_PASS_PATH
22
+ end
23
+
24
+ def client_resume_fail_url
25
+ @client_resume_fail_url ||= client_base_url + RESUME_FAIL_PATH
26
+ end
27
+
28
+ def client_smart_discovery_url
29
+ @client_smart_discovery_url ||= client_base_url + SMART_DISCOVERY_PATH
30
+ end
31
+
32
+ def client_token_url
33
+ @client_token_url ||= client_base_url + TOKEN_PATH
34
+ end
35
+
36
+ def client_suite_id
37
+ SMARTAppLaunch::SMARTClientSTU22Suite.id
38
+ end
39
+ end
40
+ end
@@ -1,4 +1,4 @@
1
1
  module SMARTAppLaunch
2
- VERSION = '0.6.0'.freeze
3
- LAST_UPDATED = '2025-03-14'.freeze
2
+ VERSION = '0.6.1'.freeze
3
+ LAST_UPDATED = '2025-04-07'.freeze
4
4
  end
@@ -5,3 +5,4 @@ require_relative 'smart_app_launch/smart_stu1_suite'
5
5
  require_relative 'smart_app_launch/smart_stu2_suite'
6
6
  require_relative 'smart_app_launch/smart_stu2_2_suite'
7
7
  require_relative 'smart_app_launch/smart_access_brands_suite'
8
+ require_relative 'smart_app_launch/client_stu2_2_suite'
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.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen MacVicar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-14 00:00:00.000000000 Z
11
+ date: 2025-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inferno_core
@@ -145,6 +145,8 @@ extensions: []
145
145
  extra_rdoc_files: []
146
146
  files:
147
147
  - LICENSE
148
+ - config/presets/SMART_RunClientAgainstServer.json.erb
149
+ - config/presets/SMART_RunServerAgainstClient.json.erb
148
150
  - config/presets/inferno_reference_server_preset.json
149
151
  - config/presets/inferno_reference_server_stu2_2_preset.json
150
152
  - config/presets/inferno_reference_server_stu2_preset.json
@@ -164,6 +166,13 @@ files:
164
166
  - lib/smart_app_launch/backend_services_invalid_grant_type_test.rb
165
167
  - lib/smart_app_launch/backend_services_invalid_jwt_test.rb
166
168
  - lib/smart_app_launch/client_assertion_builder.rb
169
+ - lib/smart_app_launch/client_stu2_2_suite.rb
170
+ - lib/smart_app_launch/client_suite/client_access_group.rb
171
+ - lib/smart_app_launch/client_suite/client_access_interaction_test.rb
172
+ - lib/smart_app_launch/client_suite/client_registration_group.rb
173
+ - lib/smart_app_launch/client_suite/client_registration_verification_test.rb
174
+ - lib/smart_app_launch/client_suite/client_token_request_verification_test.rb
175
+ - lib/smart_app_launch/client_suite/client_token_use_verification_test.rb
167
176
  - lib/smart_app_launch/code_received_test.rb
168
177
  - lib/smart_app_launch/cors_metadata_request_test.rb
169
178
  - lib/smart_app_launch/cors_openid_fhir_user_claim_test.rb
@@ -172,9 +181,14 @@ files:
172
181
  - lib/smart_app_launch/discovery_stu1_group.rb
173
182
  - lib/smart_app_launch/discovery_stu2_2_group.rb
174
183
  - lib/smart_app_launch/discovery_stu2_group.rb
184
+ - lib/smart_app_launch/docs/demo/FHIR Request.postman_collection.json
185
+ - lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md
175
186
  - lib/smart_app_launch/ehr_launch_group.rb
176
187
  - lib/smart_app_launch/ehr_launch_group_stu2.rb
177
188
  - lib/smart_app_launch/ehr_launch_group_stu2_2.rb
189
+ - lib/smart_app_launch/endpoints/echoing_fhir_responder.rb
190
+ - lib/smart_app_launch/endpoints/mock_smart_server.rb
191
+ - lib/smart_app_launch/endpoints/mock_smart_server/token.rb
178
192
  - lib/smart_app_launch/jwks.rb
179
193
  - lib/smart_app_launch/launch_received_test.rb
180
194
  - lib/smart_app_launch/metadata.rb
@@ -207,6 +221,7 @@ files:
207
221
  - lib/smart_app_launch/standalone_launch_group.rb
208
222
  - lib/smart_app_launch/standalone_launch_group_stu2.rb
209
223
  - lib/smart_app_launch/standalone_launch_group_stu2_2.rb
224
+ - lib/smart_app_launch/tags.rb
210
225
  - lib/smart_app_launch/token_exchange_stu2_2_test.rb
211
226
  - lib/smart_app_launch/token_exchange_stu2_test.rb
212
227
  - lib/smart_app_launch/token_exchange_test.rb
@@ -226,6 +241,7 @@ files:
226
241
  - lib/smart_app_launch/token_response_body_test_stu2_2.rb
227
242
  - lib/smart_app_launch/token_response_headers_test.rb
228
243
  - lib/smart_app_launch/url_helpers.rb
244
+ - lib/smart_app_launch/urls.rb
229
245
  - lib/smart_app_launch/version.rb
230
246
  - lib/smart_app_launch/well_known_capabilities_stu1_test.rb
231
247
  - lib/smart_app_launch/well_known_capabilities_stu2_test.rb