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.
- checksums.yaml +4 -4
- data/config/presets/SMART_RunClientAgainstServer.json.erb +58 -10
- data/config/presets/SMART_RunServerAgainstClient_ConfidentialAsymmetric.json.erb +183 -0
- data/config/presets/SMART_RunServerAgainstClient_ConfidentialSymmetric.json.erb +157 -0
- data/config/presets/SMART_RunServerAgainstClient_Public.json.erb +155 -0
- data/lib/smart_app_launch/backend_services_invalid_client_assertion_test.rb +1 -1
- data/lib/smart_app_launch/backend_services_invalid_jwt_test.rb +1 -1
- data/lib/smart_app_launch/client_stu2_2_suite.rb +60 -19
- data/lib/smart_app_launch/client_suite/access_alca_interaction_test.rb +75 -0
- data/lib/smart_app_launch/client_suite/access_alcs_interaction_test.rb +75 -0
- data/lib/smart_app_launch/client_suite/access_alp_interaction_test.rb +75 -0
- data/lib/smart_app_launch/client_suite/access_bsca_interaction_test.rb +46 -0
- data/lib/smart_app_launch/client_suite/access_group.rb +85 -0
- data/lib/smart_app_launch/client_suite/authentication_verification.rb +86 -0
- data/lib/smart_app_launch/client_suite/authorization_request_verification_test.rb +108 -0
- data/lib/smart_app_launch/client_suite/client_descriptions.rb +114 -0
- data/lib/smart_app_launch/client_suite/client_options.rb +35 -0
- data/lib/smart_app_launch/client_suite/oidc_jwks.json +32 -0
- data/lib/smart_app_launch/client_suite/oidc_jwks.rb +27 -0
- data/lib/smart_app_launch/client_suite/registration_alca_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/registration_alca_verification_test.rb +57 -0
- data/lib/smart_app_launch/client_suite/registration_alcs_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/registration_alcs_verification_test.rb +56 -0
- data/lib/smart_app_launch/client_suite/registration_alp_group.rb +16 -0
- data/lib/smart_app_launch/client_suite/registration_alp_verification_test.rb +50 -0
- data/lib/smart_app_launch/client_suite/registration_bsca_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/registration_bsca_verification_test.rb +40 -0
- data/lib/smart_app_launch/client_suite/registration_verification.rb +58 -0
- data/lib/smart_app_launch/client_suite/token_request_alca_verification_test.rb +53 -0
- data/lib/smart_app_launch/client_suite/token_request_alcs_verification_test.rb +53 -0
- data/lib/smart_app_launch/client_suite/token_request_alp_verification_test.rb +48 -0
- data/lib/smart_app_launch/client_suite/token_request_bsca_verification_test.rb +53 -0
- data/lib/smart_app_launch/client_suite/token_request_verification.rb +116 -0
- data/lib/smart_app_launch/client_suite/{client_token_use_verification_test.rb → token_use_verification_test.rb} +1 -8
- data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +128 -41
- data/lib/smart_app_launch/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/authorization_endpoint.rb +27 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/introspection_endpoint.rb +33 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/smart_authorization_response_creation.rb +30 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/smart_introspection_response_creation.rb +46 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/smart_token_response_creation.rb +250 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/token_endpoint.rb +58 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server.rb +128 -67
- data/lib/smart_app_launch/metadata.rb +19 -14
- data/lib/smart_app_launch/tags.rb +9 -1
- data/lib/smart_app_launch/token_payload_validation.rb +2 -2
- data/lib/smart_app_launch/urls.rb +12 -0
- data/lib/smart_app_launch/version.rb +2 -2
- metadata +38 -11
- data/config/presets/SMART_RunServerAgainstClient.json.erb +0 -42
- data/lib/smart_app_launch/client_suite/client_access_group.rb +0 -26
- data/lib/smart_app_launch/client_suite/client_access_interaction_test.rb +0 -64
- data/lib/smart_app_launch/client_suite/client_registration_group.rb +0 -15
- data/lib/smart_app_launch/client_suite/client_registration_verification_test.rb +0 -52
- data/lib/smart_app_launch/client_suite/client_token_request_verification_test.rb +0 -146
- data/lib/smart_app_launch/endpoints/echoing_fhir_responder.rb +0 -52
- data/lib/smart_app_launch/endpoints/mock_smart_server/token.rb +0 -27
@@ -1,8 +1,16 @@
|
|
1
|
-
require_relative 'endpoints/mock_smart_server/
|
2
|
-
require_relative 'endpoints/
|
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/
|
5
|
-
require_relative 'client_suite/
|
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
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|