smart_app_launch_test_kit 0.6.1 → 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 +58 -10
  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_invalid_client_assertion_test.rb +1 -1
  7. data/lib/smart_app_launch/backend_services_invalid_jwt_test.rb +1 -1
  8. data/lib/smart_app_launch/client_stu2_2_suite.rb +60 -19
  9. data/lib/smart_app_launch/client_suite/access_alca_interaction_test.rb +75 -0
  10. data/lib/smart_app_launch/client_suite/access_alcs_interaction_test.rb +75 -0
  11. data/lib/smart_app_launch/client_suite/access_alp_interaction_test.rb +75 -0
  12. data/lib/smart_app_launch/client_suite/access_bsca_interaction_test.rb +46 -0
  13. data/lib/smart_app_launch/client_suite/access_group.rb +85 -0
  14. data/lib/smart_app_launch/client_suite/authentication_verification.rb +86 -0
  15. data/lib/smart_app_launch/client_suite/authorization_request_verification_test.rb +108 -0
  16. data/lib/smart_app_launch/client_suite/client_descriptions.rb +114 -0
  17. data/lib/smart_app_launch/client_suite/client_options.rb +35 -0
  18. data/lib/smart_app_launch/client_suite/oidc_jwks.json +32 -0
  19. data/lib/smart_app_launch/client_suite/oidc_jwks.rb +27 -0
  20. data/lib/smart_app_launch/client_suite/registration_alca_group.rb +15 -0
  21. data/lib/smart_app_launch/client_suite/registration_alca_verification_test.rb +57 -0
  22. data/lib/smart_app_launch/client_suite/registration_alcs_group.rb +15 -0
  23. data/lib/smart_app_launch/client_suite/registration_alcs_verification_test.rb +56 -0
  24. data/lib/smart_app_launch/client_suite/registration_alp_group.rb +16 -0
  25. data/lib/smart_app_launch/client_suite/registration_alp_verification_test.rb +50 -0
  26. data/lib/smart_app_launch/client_suite/registration_bsca_group.rb +15 -0
  27. data/lib/smart_app_launch/client_suite/registration_bsca_verification_test.rb +40 -0
  28. data/lib/smart_app_launch/client_suite/registration_verification.rb +58 -0
  29. data/lib/smart_app_launch/client_suite/token_request_alca_verification_test.rb +53 -0
  30. data/lib/smart_app_launch/client_suite/token_request_alcs_verification_test.rb +53 -0
  31. data/lib/smart_app_launch/client_suite/token_request_alp_verification_test.rb +48 -0
  32. data/lib/smart_app_launch/client_suite/token_request_bsca_verification_test.rb +53 -0
  33. data/lib/smart_app_launch/client_suite/token_request_verification.rb +116 -0
  34. data/lib/smart_app_launch/client_suite/{client_token_use_verification_test.rb → token_use_verification_test.rb} +1 -8
  35. data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +128 -41
  36. data/lib/smart_app_launch/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
  37. data/lib/smart_app_launch/endpoints/mock_smart_server/authorization_endpoint.rb +27 -0
  38. data/lib/smart_app_launch/endpoints/mock_smart_server/introspection_endpoint.rb +33 -0
  39. data/lib/smart_app_launch/endpoints/mock_smart_server/smart_authorization_response_creation.rb +30 -0
  40. data/lib/smart_app_launch/endpoints/mock_smart_server/smart_introspection_response_creation.rb +46 -0
  41. data/lib/smart_app_launch/endpoints/mock_smart_server/smart_token_response_creation.rb +250 -0
  42. data/lib/smart_app_launch/endpoints/mock_smart_server/token_endpoint.rb +58 -0
  43. data/lib/smart_app_launch/endpoints/mock_smart_server.rb +128 -67
  44. data/lib/smart_app_launch/metadata.rb +19 -14
  45. data/lib/smart_app_launch/tags.rb +9 -1
  46. data/lib/smart_app_launch/token_payload_validation.rb +2 -2
  47. data/lib/smart_app_launch/urls.rb +12 -0
  48. data/lib/smart_app_launch/version.rb +2 -2
  49. metadata +38 -11
  50. data/config/presets/SMART_RunServerAgainstClient.json.erb +0 -42
  51. data/lib/smart_app_launch/client_suite/client_access_group.rb +0 -26
  52. data/lib/smart_app_launch/client_suite/client_access_interaction_test.rb +0 -64
  53. data/lib/smart_app_launch/client_suite/client_registration_group.rb +0 -15
  54. data/lib/smart_app_launch/client_suite/client_registration_verification_test.rb +0 -52
  55. data/lib/smart_app_launch/client_suite/client_token_request_verification_test.rb +0 -146
  56. data/lib/smart_app_launch/endpoints/echoing_fhir_responder.rb +0 -52
  57. data/lib/smart_app_launch/endpoints/mock_smart_server/token.rb +0 -27
@@ -1,8 +1,16 @@
1
- require_relative 'endpoints/mock_smart_server/token'
2
- require_relative 'endpoints/echoing_fhir_responder'
1
+ require_relative 'endpoints/mock_smart_server/token_endpoint'
2
+ require_relative 'endpoints/mock_smart_server/authorization_endpoint'
3
+ require_relative 'endpoints/mock_smart_server/introspection_endpoint'
4
+ require_relative 'endpoints/echoing_fhir_responder_endpoint'
5
+ require_relative 'tags'
3
6
  require_relative 'urls'
4
- require_relative 'client_suite/client_registration_group'
5
- require_relative 'client_suite/client_access_group'
7
+ require_relative 'client_suite/registration_alca_group'
8
+ require_relative 'client_suite/registration_alcs_group'
9
+ require_relative 'client_suite/registration_alp_group'
10
+ require_relative 'client_suite/registration_bsca_group'
11
+ require_relative 'client_suite/access_group'
12
+ require_relative 'client_suite/oidc_jwks'
13
+ require_relative 'client_suite/client_options'
6
14
 
7
15
  module SMARTAppLaunch
8
16
  class SMARTClientSTU22Suite < Inferno::TestSuite
@@ -33,7 +41,38 @@ module SMARTAppLaunch
33
41
  }
34
42
  ]
35
43
 
44
+ suite_option :client_type,
45
+ title: 'SMART Client Type',
46
+ list_options: [
47
+ {
48
+ label: 'SMART App Launch Public Client',
49
+ value: SMARTClientOptions::SMART_APP_LAUNCH_PUBLIC
50
+ },
51
+ {
52
+ label: 'SMART App Launch Confidential Symmetric Client',
53
+ value: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_SYMMETRIC
54
+ },
55
+ {
56
+ label: 'SMART App Launch Confidential Asymmetric Client',
57
+ value: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_ASYMMETRIC
58
+ },
59
+ {
60
+ label: 'SMART Backend Services Confidential Asymmetric Client',
61
+ value: SMARTClientOptions::SMART_BACKEND_SERVICES_CONFIDENTIAL_ASYMMETRIC
62
+ }
63
+ ]
64
+
36
65
  route(:get, SMART_DISCOVERY_PATH, ->(_env) {MockSMARTServer.smart_server_metadata(id) })
66
+ route(:get, OIDC_DISCOVERY_PATH, ->(_env) {MockSMARTServer.openid_connect_metadata(id) })
67
+ route(
68
+ :get,
69
+ OIDC_JWKS_PATH,
70
+ ->(_env) { [200, { 'Content-Type' => 'application/json' }, [OIDCJWKS.jwks_json]] }
71
+ )
72
+
73
+ suite_endpoint :get, AUTHORIZATION_PATH, MockSMARTServer::AuthorizationEndpoint
74
+ suite_endpoint :post, AUTHORIZATION_PATH, MockSMARTServer::AuthorizationEndpoint
75
+ suite_endpoint :post, INTROSPECTION_PATH, MockSMARTServer::IntrospectionEndpoint
37
76
  suite_endpoint :post, TOKEN_PATH, MockSMARTServer::TokenEndpoint
38
77
  suite_endpoint :get, FHIR_PATH, EchoingFHIRResponderEndpoint
39
78
  suite_endpoint :post, FHIR_PATH, EchoingFHIRResponderEndpoint
@@ -60,20 +99,22 @@ module SMARTAppLaunch
60
99
  request.query_parameters['token']
61
100
  end
62
101
 
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
102
+ group from: :smart_client_registration_alca,
103
+ required_suite_options: {
104
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_ASYMMETRIC
105
+ }
106
+ group from: :smart_client_registration_alcs,
107
+ required_suite_options: {
108
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_SYMMETRIC
109
+ }
110
+ group from: :smart_client_registration_alp,
111
+ required_suite_options: {
112
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_PUBLIC
113
+ }
114
+ group from: :smart_client_registration_bsca,
115
+ required_suite_options: {
116
+ client_type: SMARTClientOptions::SMART_BACKEND_SERVICES_CONFIDENTIAL_ASYMMETRIC
117
+ }
118
+ group from: :smart_client_access
78
119
  end
79
120
  end
@@ -0,0 +1,75 @@
1
+ require_relative '../urls'
2
+ require_relative '../endpoints/mock_smart_server'
3
+ require_relative 'client_descriptions'
4
+
5
+ module SMARTAppLaunch
6
+ class SMARTClientAccessAppLaunchConfidentialAsymmetricInteraction < Inferno::Test
7
+ include URLs
8
+ include ClientWaitDialogDescriptions
9
+
10
+ id :smart_client_access_alca_interaction
11
+ title 'Access a secured FHIR endpoint using SMART App Launch'
12
+ description %(
13
+ During this test, Inferno will wait for the confidential asymmetric client to access data
14
+ using a SMART token obtained using the SMART App Launch EHR launch
15
+ or standalone launch flow.
16
+ )
17
+ input :client_id,
18
+ title: 'Client Id',
19
+ type: 'text',
20
+ locked: true,
21
+ description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
22
+ input :smart_launch_urls,
23
+ title: 'SMART App Launch URL(s)',
24
+ type: 'textarea',
25
+ locked: true,
26
+ optional: true,
27
+ description: INPUT_SMART_LAUNCH_URLS_DESCRIPTION_LOCKED
28
+ input :launch_context,
29
+ title: 'Launch Context',
30
+ type: 'textarea',
31
+ optional: true,
32
+ description: INPUT_LAUNCH_CONTEXT_DESCRIPTION
33
+ input :fhir_user_relative_reference,
34
+ title: 'FHIR User Relative Reference',
35
+ type: 'text',
36
+ optional: true,
37
+ description: INPUT_FHIR_USER_RELATIVE_REFERENCE
38
+ input :fhir_read_resources_bundle,
39
+ title: 'Available Resources',
40
+ type: 'textarea',
41
+ optional: true,
42
+ description: INPUT_FHIR_READ_RESOURCES_BUNDLE_DESCRIPTION
43
+ input :echoed_fhir_response,
44
+ title: 'Default FHIR Response',
45
+ type: 'textarea',
46
+ optional: true,
47
+ description: INPUT_ECHOED_FHIR_RESPONSE_DESCRIPTION
48
+
49
+ output :launch_key
50
+
51
+ def client_suite_id
52
+ return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
53
+
54
+ SMARTAppLaunch::SMARTClientSTU22Suite.id
55
+ end
56
+
57
+ run do
58
+ begin
59
+ JSON.parse(launch_context) if launch_context.present?
60
+ rescue JSON::ParserError
61
+ add_message(
62
+ 'warning',
63
+ 'Input **Launch Context** is not valid JSON and will be disregarded when responding to token requests'
64
+ )
65
+ end
66
+
67
+ wait(
68
+ identifier: client_id,
69
+ message: access_wait_dialog_app_launch_access_prefix(client_id, 'confidential asymmetric', client_fhir_base_url) +
70
+ access_wait_dialog_ehr_launch_instructions(smart_launch_urls, client_fhir_base_url) +
71
+ access_wait_dialog_access_response_and_continue_suffix(client_id, client_resume_pass_url)
72
+ )
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ require_relative '../urls'
2
+ require_relative '../endpoints/mock_smart_server'
3
+ require_relative 'client_descriptions'
4
+
5
+ module SMARTAppLaunch
6
+ class SMARTClientAccessAppLaunchConfidentialSymmetricInteraction < Inferno::Test
7
+ include URLs
8
+ include ClientWaitDialogDescriptions
9
+
10
+ id :smart_client_access_alcs_interaction
11
+ title 'Access a secured FHIR endpoint using SMART App Launch'
12
+ description %(
13
+ During this test, Inferno will wait for the confidential symmetric client to access data
14
+ using a SMART token obtained using the SMART App Launch EHR launch
15
+ or standalone launch flow.
16
+ )
17
+ input :client_id,
18
+ title: 'Client Id',
19
+ type: 'text',
20
+ locked: true,
21
+ description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
22
+ input :smart_launch_urls,
23
+ title: 'SMART App Launch URL(s)',
24
+ type: 'textarea',
25
+ locked: true,
26
+ optional: true,
27
+ description: INPUT_SMART_LAUNCH_URLS_DESCRIPTION_LOCKED
28
+ input :launch_context,
29
+ title: 'Launch Context',
30
+ type: 'textarea',
31
+ optional: true,
32
+ description: INPUT_LAUNCH_CONTEXT_DESCRIPTION
33
+ input :fhir_user_relative_reference,
34
+ title: 'FHIR User Relative Reference',
35
+ type: 'text',
36
+ optional: true,
37
+ description: INPUT_FHIR_USER_RELATIVE_REFERENCE
38
+ input :fhir_read_resources_bundle,
39
+ title: 'Available Resources',
40
+ type: 'textarea',
41
+ optional: true,
42
+ description: INPUT_FHIR_READ_RESOURCES_BUNDLE_DESCRIPTION
43
+ input :echoed_fhir_response,
44
+ title: 'Default FHIR Response',
45
+ type: 'textarea',
46
+ optional: true,
47
+ description: INPUT_ECHOED_FHIR_RESPONSE_DESCRIPTION
48
+
49
+ output :launch_key
50
+
51
+ def client_suite_id
52
+ return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
53
+
54
+ SMARTAppLaunch::SMARTClientSTU22Suite.id
55
+ end
56
+
57
+ run do
58
+ begin
59
+ JSON.parse(launch_context) if launch_context.present?
60
+ rescue JSON::ParserError
61
+ add_message(
62
+ 'warning',
63
+ 'Input **Launch Context** is not valid JSON and will be disregarded when responding to token requests'
64
+ )
65
+ end
66
+
67
+ wait(
68
+ identifier: client_id,
69
+ message: access_wait_dialog_app_launch_access_prefix(client_id, 'confidential symmetric', client_fhir_base_url) +
70
+ access_wait_dialog_ehr_launch_instructions(smart_launch_urls, client_fhir_base_url) +
71
+ access_wait_dialog_access_response_and_continue_suffix(client_id, client_resume_pass_url)
72
+ )
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ require_relative '../urls'
2
+ require_relative '../endpoints/mock_smart_server'
3
+ require_relative 'client_descriptions'
4
+
5
+ module SMARTAppLaunch
6
+ class SMARTClientAccessAppLaunchPublicInteraction < Inferno::Test
7
+ include URLs
8
+ include ClientWaitDialogDescriptions
9
+
10
+ id :smart_client_access_alp_interaction
11
+ title 'Access a secured FHIR endpoint using SMART App Launch'
12
+ description %(
13
+ During this test, Inferno will wait for the public client to access data
14
+ using a SMART token obtained using the SMART App Launch EHR launch
15
+ or standalone launch flow.
16
+ )
17
+ input :client_id,
18
+ title: 'Client Id',
19
+ type: 'text',
20
+ locked: true,
21
+ description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
22
+ input :smart_launch_urls,
23
+ title: 'SMART App Launch URL(s)',
24
+ type: 'textarea',
25
+ locked: true,
26
+ optional: true,
27
+ description: INPUT_SMART_LAUNCH_URLS_DESCRIPTION_LOCKED
28
+ input :launch_context,
29
+ title: 'Launch Context',
30
+ type: 'textarea',
31
+ optional: true,
32
+ description: INPUT_LAUNCH_CONTEXT_DESCRIPTION
33
+ input :fhir_user_relative_reference,
34
+ title: 'FHIR User Relative Reference',
35
+ type: 'text',
36
+ optional: true,
37
+ description: INPUT_FHIR_USER_RELATIVE_REFERENCE
38
+ input :fhir_read_resources_bundle,
39
+ title: 'Available Resources',
40
+ type: 'textarea',
41
+ optional: true,
42
+ description: INPUT_FHIR_READ_RESOURCES_BUNDLE_DESCRIPTION
43
+ input :echoed_fhir_response,
44
+ title: 'Default FHIR Response',
45
+ type: 'textarea',
46
+ optional: true,
47
+ description: INPUT_ECHOED_FHIR_RESPONSE_DESCRIPTION
48
+
49
+ output :launch_key
50
+
51
+ def client_suite_id
52
+ return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
53
+
54
+ SMARTAppLaunch::SMARTClientSTU22Suite.id
55
+ end
56
+
57
+ run do
58
+ begin
59
+ JSON.parse(launch_context) if launch_context.present?
60
+ rescue JSON::ParserError
61
+ add_message(
62
+ 'warning',
63
+ 'Input **Launch Context** is not valid JSON and will be disregarded when responding to token requests'
64
+ )
65
+ end
66
+
67
+ wait(
68
+ identifier: client_id,
69
+ message: access_wait_dialog_app_launch_access_prefix(client_id, 'public', client_fhir_base_url) +
70
+ access_wait_dialog_ehr_launch_instructions(smart_launch_urls, client_fhir_base_url) +
71
+ access_wait_dialog_access_response_and_continue_suffix(client_id, client_resume_pass_url)
72
+ )
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,46 @@
1
+ require_relative '../urls'
2
+ require_relative '../endpoints/mock_smart_server'
3
+ require_relative 'client_descriptions'
4
+
5
+ module SMARTAppLaunch
6
+ class SMARTClientAccessBackendServicesConfidentialAsymmetricInteraction < Inferno::Test
7
+ include URLs
8
+ include ClientWaitDialogDescriptions
9
+
10
+ id :smart_client_access_bsca_interaction
11
+ title 'Access a secured FHIR endpoint using SMART Backend Services'
12
+ description %(
13
+ During this test, Inferno will wait for the client to access data
14
+ using a SMART token obtained using the Backend Services flow.
15
+ )
16
+ input :client_id,
17
+ title: 'Client Id',
18
+ type: 'text',
19
+ locked: true,
20
+ description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
21
+ input :fhir_read_resources_bundle,
22
+ title: 'Available Resources',
23
+ type: 'textarea',
24
+ optional: true,
25
+ description: INPUT_FHIR_READ_RESOURCES_BUNDLE_DESCRIPTION
26
+ input :echoed_fhir_response,
27
+ title: 'Default FHIR Response',
28
+ type: 'textarea',
29
+ optional: true,
30
+ description: INPUT_ECHOED_FHIR_RESPONSE_DESCRIPTION
31
+
32
+ def client_suite_id
33
+ return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
34
+
35
+ SMARTAppLaunch::SMARTClientSTU22Suite.id
36
+ end
37
+
38
+ run do
39
+ wait(
40
+ identifier: client_id,
41
+ message: access_wait_dialog_backend_services_access_prefix(client_id, client_fhir_base_url) +
42
+ access_wait_dialog_access_response_and_continue_suffix(client_id, client_resume_pass_url)
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,85 @@
1
+ require_relative 'access_alca_interaction_test'
2
+ require_relative 'access_alcs_interaction_test'
3
+ require_relative 'access_alp_interaction_test'
4
+ require_relative 'access_bsca_interaction_test'
5
+ require_relative 'authorization_request_verification_test'
6
+ require_relative 'token_request_alca_verification_test'
7
+ require_relative 'token_request_alcs_verification_test'
8
+ require_relative 'token_request_alp_verification_test'
9
+ require_relative 'token_request_bsca_verification_test'
10
+ require_relative 'token_use_verification_test'
11
+ require_relative '../tags'
12
+
13
+ module SMARTAppLaunch
14
+ class SMARTClientAccess < Inferno::TestGroup
15
+ id :smart_client_access
16
+ title 'Client Access'
17
+ description %(
18
+ During these tests, the client system will access Inferno's simulated
19
+ FHIR server that is protected using SMART. The client will request
20
+ an access token using the OAuth flow setup during registration and will then
21
+ make a FHIR request using that token.
22
+
23
+ Inferno will then verify that any OAuth requests made were conformant
24
+ and that a token returned from a token request was used on an access request.
25
+ )
26
+
27
+ run_as_group
28
+
29
+ # Access Interaction Test (All, different for each)
30
+ test from: :smart_client_access_alca_interaction,
31
+ required_suite_options: {
32
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_ASYMMETRIC
33
+ }
34
+ test from: :smart_client_access_alcs_interaction,
35
+ required_suite_options: {
36
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_SYMMETRIC
37
+ }
38
+ test from: :smart_client_access_alp_interaction,
39
+ required_suite_options: {
40
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_PUBLIC
41
+ }
42
+ test from: :smart_client_access_bsca_interaction,
43
+ required_suite_options: {
44
+ client_type: SMARTClientOptions::SMART_BACKEND_SERVICES_CONFIDENTIAL_ASYMMETRIC
45
+ }
46
+
47
+ # Authorization Request Verification (app launch only, same for each)
48
+ test from: :smart_client_authorization_request_verification,
49
+ id: :smart_client_authorization_request_alca_verification,
50
+ required_suite_options: {
51
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_ASYMMETRIC
52
+ }
53
+ test from: :smart_client_authorization_request_verification,
54
+ id: :smart_client_authorization_request_alcs_verification,
55
+ required_suite_options: {
56
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_SYMMETRIC
57
+ }
58
+ test from: :smart_client_authorization_request_verification,
59
+ id: :smart_client_authorization_request_alp_verification,
60
+ required_suite_options: {
61
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_PUBLIC
62
+ }
63
+
64
+ # Access token request (All, different for each)
65
+ test from: :smart_client_token_request_alca_verification,
66
+ required_suite_options: {
67
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_ASYMMETRIC
68
+ }
69
+ test from: :smart_client_token_request_alcs_verification,
70
+ required_suite_options: {
71
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_CONFIDENTIAL_SYMMETRIC
72
+ }
73
+ test from: :smart_client_token_request_alp_verification,
74
+ required_suite_options: {
75
+ client_type: SMARTClientOptions::SMART_APP_LAUNCH_PUBLIC
76
+ }
77
+ test from: :smart_client_token_request_bsca_verification,
78
+ required_suite_options: {
79
+ client_type: SMARTClientOptions::SMART_BACKEND_SERVICES_CONFIDENTIAL_ASYMMETRIC
80
+ }
81
+
82
+ # Access token use (all the same)
83
+ test from: :smart_client_token_use_verification
84
+ end
85
+ end
@@ -0,0 +1,86 @@
1
+ require 'jwt'
2
+ require_relative 'client_options'
3
+ require_relative '../endpoints/mock_smart_server'
4
+
5
+ module SMARTAppLaunch
6
+ module AuthenticationVerification
7
+ def check_authentication(authentication_apporach, request, request_params, jti_list, request_num)
8
+ case authentication_apporach
9
+ when CONFIDENTIAL_ASYMMETRIC_TAG
10
+ check_client_assertion(request_params['client_assertion'], jti_list, request_num)
11
+ when CONFIDENTIAL_SYMMETRIC_TAG
12
+ check_authorization_header(request, request_num)
13
+ end
14
+ end
15
+
16
+ def check_authorization_header(request, request_num)
17
+ authorization_header_value = request.request_header('authorization')&.value
18
+ error = MockSMARTServer.confidential_symmetric_header_value_error(authorization_header_value, client_id,
19
+ smart_client_secret)
20
+ if error.present?
21
+ add_message('error', "Token request #{request_num} invalid: #{e}")
22
+ end
23
+ end
24
+
25
+ def check_client_assertion(assertion, jti_list, request_num)
26
+ decoded_token =
27
+ begin
28
+ JWT::EncodedToken.new(assertion)
29
+ rescue StandardError => e
30
+ add_message('error', "Token request #{request_num} contained an invalid client assertion jwt: #{e}")
31
+ nil
32
+ end
33
+
34
+ return unless decoded_token.present?
35
+
36
+ check_jwt_header(decoded_token.header, request_num)
37
+ check_jwt_payload(decoded_token.payload, jti_list, request_num)
38
+ check_jwt_signature(decoded_token, request_num)
39
+ end
40
+
41
+ def check_jwt_header(header, request_num)
42
+ return unless header['typ'] != 'JWT'
43
+
44
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `typ` header: " \
45
+ "expected 'JWT', got '#{header['typ']}'")
46
+ end
47
+
48
+ def check_jwt_payload(claims, jti_list, request_num)
49
+ if claims['iss'] != client_id
50
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `iss` claim: " \
51
+ "expected '#{client_id}', got '#{claims['iss']}'")
52
+ end
53
+
54
+ if claims['sub'] != client_id
55
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `sub` claim: " \
56
+ "expected '#{client_id}', got '#{claims['sub']}'")
57
+ end
58
+
59
+ if claims['aud'] != client_token_url
60
+ add_message('error', "client assertion jwt on token request #{request_num} has an incorrect `aud` claim: " \
61
+ "expected '#{client_token_url}', got '#{claims['aud']}'")
62
+ end
63
+
64
+ if claims['exp'].blank?
65
+ add_message('error', "client assertion jwt on token request #{request_num} is missing the `exp` claim.")
66
+ end
67
+
68
+ if claims['jti'].blank?
69
+ add_message('error', "client assertion jwt on token request #{request_num} is missing the `jti` claim.")
70
+ elsif jti_list.include?(claims['jti'])
71
+ add_message('error', "client assertion jwt on token request #{request_num} has a `jti` claim that was " \
72
+ "previouly used: '#{claims['jti']}'.")
73
+ else
74
+ jti_list << claims['jti']
75
+ end
76
+ end
77
+
78
+ def check_jwt_signature(encoded_token, request_num)
79
+ error = MockSMARTServer.smart_assertion_signature_verification(encoded_token, smart_jwk_set)
80
+
81
+ return unless error.present?
82
+
83
+ add_message('error', "Signature validation failed on token request #{request_num}: #{error}")
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,108 @@
1
+ require_relative '../tags'
2
+ require_relative '../urls'
3
+ require_relative '../endpoints/mock_smart_server'
4
+ require_relative 'client_descriptions'
5
+
6
+ module SMARTAppLaunch
7
+ class SMARTClientAppLaunchAuthorizationRequestVerification < Inferno::Test
8
+ include URLs
9
+
10
+ id :smart_client_authorization_request_verification
11
+ title 'Verify SMART App Launch Authorization Requests'
12
+ description %(
13
+ Check that SMART authorization requests made are conformant.
14
+ )
15
+
16
+ input :client_id,
17
+ title: 'Client Id',
18
+ type: 'text',
19
+ locked: true,
20
+ description: INPUT_CLIENT_ID_DESCRIPTION_LOCKED
21
+ input :smart_redirect_uris,
22
+ title: 'SMART App Launch Redirect URI(s)',
23
+ type: 'textarea',
24
+ locked: true,
25
+ description: INPUT_SMART_REDIRECT_URIS_DESCRIPTION_LOCKED
26
+ input :launch_key, # from app launch access interaction test,
27
+ optional: true # present if client registered for ehr launch but won't know if did ehr or standalone
28
+
29
+ def client_suite_id
30
+ return config.options[:endpoint_suite_id] if config.options[:endpoint_suite_id].present?
31
+
32
+ SMARTAppLaunch::SMARTClientSTU22Suite.id
33
+ end
34
+
35
+ run do
36
+ load_tagged_requests(AUTHORIZATION_TAG, SMART_TAG)
37
+ skip_if requests.blank?, 'No SMART authorization requests made.'
38
+
39
+ requests.each_with_index do |authorization_request, index|
40
+ auth_code_request_params = MockSMARTServer.authorization_code_request_details(authorization_request)
41
+ check_request_params(auth_code_request_params, index + 1)
42
+ end
43
+
44
+ assert messages.none? { |msg|
45
+ msg[:type] == 'error'
46
+ }, 'Invalid authorization requests detected. See messages for details.'
47
+ end
48
+
49
+ def check_request_params(params, request_num)
50
+ if params['response_type'] != 'code'
51
+ add_message('error',
52
+ "Authorization request #{request_num} had an incorrect `response_type`: expected 'code', " \
53
+ "but got '#{params['response_type']}'")
54
+ end
55
+ if params['client_id'] != client_id
56
+ add_message('error',
57
+ "Authorization request #{request_num} had an incorrect `client_id`: expected #{client_id}, " \
58
+ "but got '#{params['client_id']}'")
59
+ end
60
+ if params['redirect_uri'].blank?
61
+ add_message('error',
62
+ "Authorization request #{request_num} is missing the `redirect_uri` element")
63
+ else
64
+ if smart_redirect_uris.blank?
65
+ add_message('error',
66
+ 'No redirect URIs registered to check against the `redirect_uri` element ' \
67
+ "in authorization request #{request_num} is missing the `redirect_uri` element")
68
+ elsif !smart_redirect_uris.split(',').include?(params['redirect_uri'])
69
+ add_message('error',
70
+ "Authorization request #{request_num} had an unregistered `redirect_uri`: " \
71
+ "got #{params['redirect_uri']}, but expected one of '#{smart_redirect_uris}'")
72
+ end
73
+ end
74
+ # for ehr launch, `launch` value must be the one Inferno generated
75
+ # but can't know if this was intended to be ehr or standalone if `launch` isn't there
76
+ # and currently the tests allow either standalone or ehr launch
77
+ if launch_key.present? && params['launch'].present? && params['launch'] != launch_key
78
+ add_message('error',
79
+ "Authorization request #{request_num} had an incorrect `launch`: expected #{launch_key}, " \
80
+ "but got '#{params['launch']}'")
81
+ end
82
+
83
+ if params['state'].blank?
84
+ add_message('error',
85
+ "Authorization request #{request_num} is missing the `state` element")
86
+ end
87
+ if params['aud'] != client_fhir_base_url
88
+ add_message('error',
89
+ "Authorization request #{request_num} had an incorrect `aud`: " \
90
+ "expected '#{client_fhir_base_url}', but got '#{params['aud']}'")
91
+ end
92
+ if params['code_challenge'].blank?
93
+ add_message('error',
94
+ "Authorization request #{request_num} is missing the `code_challenge` element")
95
+ end
96
+ if params['code_challenge_method'] != 'S256'
97
+ add_message('error',
98
+ "Authorization request #{request_num} had an incorrect `code_challenge_method`: " \
99
+ "expected 'S256', but got '#{params['code_challenge_method']}'")
100
+ end
101
+ if params['scope'].blank?
102
+ add_message('error', "Token request #{request_num} did not include the requested `scope`")
103
+ end
104
+
105
+ nil
106
+ end
107
+ end
108
+ end