smart_app_launch_test_kit 0.0.1 → 0.1.0
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/lib/smart_app_launch/app_launch_test.rb +1 -1
- data/lib/smart_app_launch/app_redirect_test.rb +68 -8
- data/lib/smart_app_launch/discovery_group.rb +2 -2
- data/lib/smart_app_launch/ehr_launch_group.rb +33 -17
- data/lib/smart_app_launch/openid_connect_group.rb +1 -25
- data/lib/smart_app_launch/openid_fhir_user_claim_test.rb +24 -7
- data/lib/smart_app_launch/standalone_launch_group.rb +34 -17
- data/lib/smart_app_launch/token_exchange_test.rb +36 -2
- data/lib/smart_app_launch/token_refresh_body_test.rb +1 -10
- data/lib/smart_app_launch/token_refresh_group.rb +1 -0
- data/lib/smart_app_launch/token_refresh_test.rb +15 -6
- data/lib/smart_app_launch/version.rb +3 -0
- data/lib/smart_app_launch_test_kit.rb +42 -9
- metadata +26 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7da125d1c3d1b6665088989e563aa9a046fc9e651ab3b1616e153cee1d695a6e
|
4
|
+
data.tar.gz: 329bf68903d2b72988bfbdd1c0525ec1a933c5450b285fc001c4d28d44552f2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '081f7d57660ef9446376fd1fd07753b00014258f9162b3ffc14f57165caa4ca255d3dc970483b30236a7fd9bbfec49c0bda9becb6c3a20f6805e679b6fc24814'
|
7
|
+
data.tar.gz: ee1f2ea1d9b75980516d2098528b229eabbacea1fc305c1242aacff60e1fbd7bf06a2d7b98788f313c000492264e9f5ef79b120ebeb0bd7e7e124e3147a48a47
|
@@ -10,7 +10,7 @@ module SMARTAppLaunch
|
|
10
10
|
input :url
|
11
11
|
receives_request :launch
|
12
12
|
|
13
|
-
config options: { launch_uri: "#{Inferno::Application['
|
13
|
+
config options: { launch_uri: "#{Inferno::Application['base_url']}/custom/smart/launch" }
|
14
14
|
|
15
15
|
run do
|
16
16
|
wait(
|
@@ -8,13 +8,63 @@ module SMARTAppLaunch
|
|
8
8
|
id :smart_app_redirect
|
9
9
|
|
10
10
|
input :client_id, :requested_scopes, :url, :smart_authorization_url
|
11
|
+
input :use_pkce,
|
12
|
+
title: 'Proof Key for Code Exchange (PKCE)',
|
13
|
+
type: 'radio',
|
14
|
+
default: 'false',
|
15
|
+
options: {
|
16
|
+
list_options: [
|
17
|
+
{
|
18
|
+
label: 'Enabled',
|
19
|
+
value: 'true'
|
20
|
+
},
|
21
|
+
{
|
22
|
+
label: 'Disabled',
|
23
|
+
value: 'false'
|
24
|
+
}
|
25
|
+
]
|
26
|
+
}
|
27
|
+
input :pkce_code_challenge_method,
|
28
|
+
optional: true,
|
29
|
+
title: 'PKCE Code Challenge Method',
|
30
|
+
type: 'radio',
|
31
|
+
default: 'S256',
|
32
|
+
options: {
|
33
|
+
list_options: [
|
34
|
+
{
|
35
|
+
label: 'S256',
|
36
|
+
value: 'S256'
|
37
|
+
},
|
38
|
+
{
|
39
|
+
label: 'plain',
|
40
|
+
value: 'plain'
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
11
44
|
|
12
|
-
output :state
|
45
|
+
output :state, :pkce_code_challenge, :pkce_code_verifier
|
13
46
|
receives_request :redirect
|
14
47
|
|
15
|
-
config options: { redirect_uri: "#{Inferno::Application['
|
48
|
+
config options: { redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect" }
|
49
|
+
|
50
|
+
def self.calculate_s256_challenge(verifier)
|
51
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
52
|
+
end
|
53
|
+
|
54
|
+
def aud
|
55
|
+
url
|
56
|
+
end
|
57
|
+
|
58
|
+
def wait_message(auth_url)
|
59
|
+
%(
|
60
|
+
[Follow this link to authorize with the SMART server](#{auth_url}).
|
61
|
+
Waiting to receive a request at `#{config.options[:redirect_uri]}` with
|
62
|
+
a state of `#{state}`.
|
63
|
+
)
|
64
|
+
end
|
16
65
|
|
17
66
|
run do
|
67
|
+
info(config.options[:redirect_uri])
|
18
68
|
assert_valid_http_uri(
|
19
69
|
smart_authorization_url,
|
20
70
|
"OAuth2 Authorization Endpoint '#{smart_authorization_url}' is not a valid URI"
|
@@ -28,11 +78,25 @@ module SMARTAppLaunch
|
|
28
78
|
'redirect_uri' => config.options[:redirect_uri],
|
29
79
|
'scope' => requested_scopes,
|
30
80
|
'state' => state,
|
31
|
-
'aud' =>
|
81
|
+
'aud' => aud
|
32
82
|
}
|
33
83
|
|
34
84
|
oauth2_params['launch'] = launch if self.class.inputs.include?(:launch)
|
35
85
|
|
86
|
+
if use_pkce == 'true'
|
87
|
+
code_verifier = SecureRandom.uuid
|
88
|
+
code_challenge =
|
89
|
+
if pkce_code_challenge_method == 'S256'
|
90
|
+
self.class.calculate_s256_challenge(code_verifier)
|
91
|
+
else
|
92
|
+
code_verifier
|
93
|
+
end
|
94
|
+
|
95
|
+
output pkce_code_verifier: code_verifier, pkce_code_challenge: code_challenge
|
96
|
+
|
97
|
+
oauth2_params.merge!('code_challenge' => code_challenge, 'code_challenge_method' => pkce_code_challenge_method)
|
98
|
+
end
|
99
|
+
|
36
100
|
authorization_url = smart_authorization_url
|
37
101
|
|
38
102
|
authorization_url +=
|
@@ -50,11 +114,7 @@ module SMARTAppLaunch
|
|
50
114
|
|
51
115
|
wait(
|
52
116
|
identifier: state,
|
53
|
-
message:
|
54
|
-
[Follow this link to authorize with the SMART
|
55
|
-
server](#{authorization_url}). Waiting to receive a request at
|
56
|
-
`#{config.options[:redirect_uri]}` with a state of `#{state}`.
|
57
|
-
)
|
117
|
+
message: wait_message(authorization_url)
|
58
118
|
)
|
59
119
|
end
|
60
120
|
end
|
@@ -2,6 +2,7 @@ module SMARTAppLaunch
|
|
2
2
|
class DiscoveryGroup < Inferno::TestGroup
|
3
3
|
id :smart_discovery
|
4
4
|
title 'SMART on FHIR Discovery'
|
5
|
+
short_description 'Retrieve server\'s SMART on FHIR configuration.'
|
5
6
|
description %(
|
6
7
|
# Background
|
7
8
|
|
@@ -43,8 +44,7 @@ module SMARTAppLaunch
|
|
43
44
|
)
|
44
45
|
input :url,
|
45
46
|
title: 'FHIR Endpoint',
|
46
|
-
description: 'URL of the FHIR endpoint used by SMART applications'
|
47
|
-
default: 'https://inferno.healthit.gov/reference-server/r4'
|
47
|
+
description: 'URL of the FHIR endpoint used by SMART applications'
|
48
48
|
output :well_known_configuration,
|
49
49
|
:well_known_authorization_url,
|
50
50
|
:well_known_introspection_url,
|
@@ -10,6 +10,7 @@ module SMARTAppLaunch
|
|
10
10
|
class EHRLaunchGroup < Inferno::TestGroup
|
11
11
|
id :smart_ehr_launch
|
12
12
|
title 'SMART EHR Launch'
|
13
|
+
short_description 'Demonstrate the ability to authorize an app using the EHR Launch.'
|
13
14
|
|
14
15
|
description %(
|
15
16
|
# Background
|
@@ -42,8 +43,7 @@ module SMARTAppLaunch
|
|
42
43
|
client_id: {
|
43
44
|
name: :ehr_client_id,
|
44
45
|
title: 'EHR Launch Client ID',
|
45
|
-
description: 'Client ID provided during registration of Inferno as an EHR launch application'
|
46
|
-
default: 'SAMPLE_PUBLIC_CLIENT_ID'
|
46
|
+
description: 'Client ID provided during registration of Inferno as an EHR launch application'
|
47
47
|
},
|
48
48
|
client_secret: {
|
49
49
|
name: :ehr_client_secret,
|
@@ -55,23 +55,11 @@ module SMARTAppLaunch
|
|
55
55
|
title: 'EHR Launch Scope',
|
56
56
|
description: 'OAuth 2.0 scope provided by system to enable all required functionality',
|
57
57
|
type: 'textarea',
|
58
|
-
default:
|
59
|
-
launch openid fhirUser offline_access
|
60
|
-
patient/Medication.read patient/AllergyIntolerance.read
|
61
|
-
patient/CarePlan.read patient/CareTeam.read patient/Condition.read
|
62
|
-
patient/Device.read patient/DiagnosticReport.read
|
63
|
-
patient/DocumentReference.read patient/Encounter.read
|
64
|
-
patient/Goal.read patient/Immunization.read patient/Location.read
|
65
|
-
patient/MedicationRequest.read patient/Observation.read
|
66
|
-
patient/Organization.read patient/Patient.read
|
67
|
-
patient/Practitioner.read patient/Procedure.read
|
68
|
-
patient/Provenance.read patient/PractitionerRole.read
|
69
|
-
).gsub(/\s{2,}/, ' ').strip
|
58
|
+
default: 'launch openid fhirUser offline_access user/*.read'
|
70
59
|
},
|
71
60
|
url: {
|
72
61
|
title: 'EHR Launch FHIR Endpoint',
|
73
|
-
description: 'URL of the FHIR endpoint used by EHR launched applications'
|
74
|
-
default: 'https://inferno.healthit.gov/reference-server/r4'
|
62
|
+
description: 'URL of the FHIR endpoint used by EHR launched applications'
|
75
63
|
},
|
76
64
|
code: {
|
77
65
|
name: :ehr_code
|
@@ -81,6 +69,9 @@ module SMARTAppLaunch
|
|
81
69
|
},
|
82
70
|
launch: {
|
83
71
|
name: :ehr_launch
|
72
|
+
},
|
73
|
+
smart_credentials: {
|
74
|
+
name: :ehr_smart_credentials
|
84
75
|
}
|
85
76
|
},
|
86
77
|
outputs: {
|
@@ -95,7 +86,8 @@ module SMARTAppLaunch
|
|
95
86
|
patient_id: { name: :ehr_patient_id },
|
96
87
|
encounter_id: { name: :ehr_encounter_id },
|
97
88
|
received_scopes: { name: :ehr_received_scopes },
|
98
|
-
intent: { name: :ehr_intent }
|
89
|
+
intent: { name: :ehr_intent },
|
90
|
+
smart_credentials: { name: :ehr_smart_credentials }
|
99
91
|
},
|
100
92
|
requests: {
|
101
93
|
launch: { name: :ehr_launch },
|
@@ -106,10 +98,34 @@ module SMARTAppLaunch
|
|
106
98
|
|
107
99
|
test from: :smart_app_launch
|
108
100
|
test from: :smart_launch_received
|
101
|
+
test from: :tls_version_test,
|
102
|
+
id: :ehr_auth_tls,
|
103
|
+
title: 'OAuth 2.0 authorize endpoint secured by transport layer security',
|
104
|
+
description: %(
|
105
|
+
Apps MUST assure that sensitive information (authentication secrets,
|
106
|
+
authorization codes, tokens) is transmitted ONLY to authenticated
|
107
|
+
servers, over TLS-secured channels.
|
108
|
+
),
|
109
|
+
config: {
|
110
|
+
inputs: { url: { name: :smart_authorization_url } },
|
111
|
+
options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
|
112
|
+
}
|
109
113
|
test from: :smart_app_redirect do
|
110
114
|
input :launch
|
111
115
|
end
|
112
116
|
test from: :smart_code_received
|
117
|
+
test from: :tls_version_test,
|
118
|
+
id: :ehr_token_tls,
|
119
|
+
title: 'OAuth 2.0 token endpoint secured by transport layer security',
|
120
|
+
description: %(
|
121
|
+
Apps MUST assure that sensitive information (authentication secrets,
|
122
|
+
authorization codes, tokens) is transmitted ONLY to authenticated
|
123
|
+
servers, over TLS-secured channels.
|
124
|
+
),
|
125
|
+
config: {
|
126
|
+
inputs: { url: { name: :smart_token_url } },
|
127
|
+
options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
|
128
|
+
}
|
113
129
|
test from: :smart_token_exchange
|
114
130
|
test from: :smart_token_response_body
|
115
131
|
test from: :smart_token_response_headers
|
@@ -11,6 +11,7 @@ module SMARTAppLaunch
|
|
11
11
|
class OpenIDConnectGroup < Inferno::TestGroup
|
12
12
|
id :smart_openid_connect
|
13
13
|
title 'OpenID Connect'
|
14
|
+
short_description 'Demonstrate the ability to authenticate users with OpenID Connect.'
|
14
15
|
|
15
16
|
description %(
|
16
17
|
# Background
|
@@ -54,30 +55,5 @@ module SMARTAppLaunch
|
|
54
55
|
test from: :smart_openid_token_payload
|
55
56
|
|
56
57
|
test from: :smart_openid_fhir_user_claim
|
57
|
-
|
58
|
-
# test do
|
59
|
-
# id :smart_openid_fhir_user_retrieval
|
60
|
-
# title 'fhirUser can be retrieved'
|
61
|
-
# description %(
|
62
|
-
# Verify that the FHIR resource referred to in the `fhirUser` claim can be
|
63
|
-
# retrieved.
|
64
|
-
# )
|
65
|
-
|
66
|
-
# input :id_token_fhir_user, :openid_issuer, :standalone_access_token
|
67
|
-
# makes_request :id_token_fhir_user
|
68
|
-
|
69
|
-
# run do
|
70
|
-
# skip_if id_token_fhir_user.blank?
|
71
|
-
|
72
|
-
# split_fhir_user = id_token_fhir_user.split('/')
|
73
|
-
# resource_type = split_fhir_user[-2]
|
74
|
-
# resource_id = split_fhir_user[-1]
|
75
|
-
# fhir_read(resource_type, resource_id)
|
76
|
-
|
77
|
-
# assert_response_status(200)
|
78
|
-
# assert_valid_json(response[:body])
|
79
|
-
# assert_resource_type(resource_type)
|
80
|
-
# end
|
81
|
-
# end
|
82
58
|
end
|
83
59
|
end
|
@@ -1,30 +1,47 @@
|
|
1
1
|
module SMARTAppLaunch
|
2
2
|
class OpenIDFHIRUserClaimTest < Inferno::Test
|
3
3
|
id :smart_openid_fhir_user_claim
|
4
|
-
title '
|
4
|
+
title 'FHIR resource representing the current user can be retrieved'
|
5
5
|
description %(
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
Verify that the `fhirUser` claim is present in the ID token and that the
|
7
|
+
FHIR resource it refers to can be retrieved. The `fhirUser` claim must be
|
8
|
+
the url for a Patient, Practitioner, RelatedPerson, or Person resource
|
9
|
+
)
|
10
10
|
|
11
|
-
input :id_token_payload_json, :requested_scopes
|
11
|
+
input :id_token_payload_json, :requested_scopes, :url
|
12
|
+
input :smart_credentials, type: :oauth_credentials
|
12
13
|
output :id_token_fhir_user
|
13
14
|
|
15
|
+
fhir_client do
|
16
|
+
url :url
|
17
|
+
oauth_credentials :smart_credentials
|
18
|
+
end
|
19
|
+
|
14
20
|
run do
|
15
21
|
skip_if id_token_payload_json.blank?
|
16
22
|
skip_if !requested_scopes&.include?('fhirUser'), '`fhirUser` scope not requested'
|
17
23
|
|
24
|
+
assert_valid_json(id_token_payload_json)
|
18
25
|
payload = JSON.parse(id_token_payload_json)
|
19
26
|
fhir_user = payload['fhirUser']
|
20
27
|
|
21
28
|
valid_fhir_user_resource_types = ['Patient', 'Practitioner', 'RelatedPerson', 'Person']
|
22
29
|
|
23
30
|
assert fhir_user.present?, 'ID token does not contain `fhirUser` claim'
|
24
|
-
|
31
|
+
|
32
|
+
fhir_user_segments = fhir_user.split('/')
|
33
|
+
fhir_user_resource_type = fhir_user_segments[-2]
|
34
|
+
fhir_user_id = fhir_user_segments.last
|
35
|
+
|
36
|
+
assert valid_fhir_user_resource_types.include?(fhir_user_resource_type),
|
25
37
|
"ID token `fhirUser` claim does not refer to a valid resource type: #{fhir_user}"
|
26
38
|
|
27
39
|
output id_token_fhir_user: fhir_user
|
40
|
+
|
41
|
+
fhir_read(fhir_user_resource_type, fhir_user_id)
|
42
|
+
|
43
|
+
assert_response_status(200)
|
44
|
+
assert_resource_type(fhir_user_resource_type)
|
28
45
|
end
|
29
46
|
end
|
30
47
|
end
|
@@ -8,6 +8,7 @@ module SMARTAppLaunch
|
|
8
8
|
class StandaloneLaunchGroup < Inferno::TestGroup
|
9
9
|
id :smart_standalone_launch
|
10
10
|
title 'SMART Standalone Launch'
|
11
|
+
short_description 'Demonstrate the ability to authorize an app using the Standalone Launch.'
|
11
12
|
|
12
13
|
description %(
|
13
14
|
# Background
|
@@ -38,8 +39,7 @@ module SMARTAppLaunch
|
|
38
39
|
client_id: {
|
39
40
|
name: :standalone_client_id,
|
40
41
|
title: 'Standalone Client ID',
|
41
|
-
description: 'Client ID provided during registration of Inferno as a standalone application'
|
42
|
-
default: 'SAMPLE_PUBLIC_CLIENT_ID'
|
42
|
+
description: 'Client ID provided during registration of Inferno as a standalone application'
|
43
43
|
},
|
44
44
|
client_secret: {
|
45
45
|
name: :standalone_client_secret,
|
@@ -51,30 +51,22 @@ module SMARTAppLaunch
|
|
51
51
|
title: 'Standalone Scope',
|
52
52
|
description: 'OAuth 2.0 scope provided by system to enable all required functionality',
|
53
53
|
type: 'textarea',
|
54
|
-
default:
|
55
|
-
launch/patient openid fhirUser offline_access
|
56
|
-
patient/Medication.read patient/AllergyIntolerance.read
|
57
|
-
patient/CarePlan.read patient/CareTeam.read patient/Condition.read
|
58
|
-
patient/Device.read patient/DiagnosticReport.read
|
59
|
-
patient/DocumentReference.read patient/Encounter.read
|
60
|
-
patient/Goal.read patient/Immunization.read patient/Location.read
|
61
|
-
patient/MedicationRequest.read patient/Observation.read
|
62
|
-
patient/Organization.read patient/Patient.read
|
63
|
-
patient/Practitioner.read patient/Procedure.read
|
64
|
-
patient/Provenance.read patient/PractitionerRole.read
|
65
|
-
).gsub(/\s{2,}/, ' ').strip
|
54
|
+
default: 'launch/patient openid fhirUser offline_access patient/*.read'
|
66
55
|
},
|
67
56
|
url: {
|
68
57
|
title: 'Standalone FHIR Endpoint',
|
69
|
-
description: 'URL of the FHIR endpoint used by standalone applications'
|
70
|
-
default: 'https://inferno.healthit.gov/reference-server/r4'
|
58
|
+
description: 'URL of the FHIR endpoint used by standalone applications'
|
71
59
|
},
|
72
60
|
code: {
|
73
61
|
name: :standalone_code
|
74
62
|
},
|
75
63
|
state: {
|
76
64
|
name: :standalone_state
|
65
|
+
},
|
66
|
+
smart_credentials: {
|
67
|
+
name: :standalone_smart_credentials
|
77
68
|
}
|
69
|
+
|
78
70
|
},
|
79
71
|
outputs: {
|
80
72
|
code: { name: :standalone_code },
|
@@ -87,7 +79,8 @@ module SMARTAppLaunch
|
|
87
79
|
patient_id: { name: :standalone_patient_id },
|
88
80
|
encounter_id: { name: :standalone_encounter_id },
|
89
81
|
received_scopes: { name: :standalone_received_scopes },
|
90
|
-
intent: { name: :standalone_intent }
|
82
|
+
intent: { name: :standalone_intent },
|
83
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
91
84
|
},
|
92
85
|
requests: {
|
93
86
|
redirect: { name: :standalone_redirect },
|
@@ -95,8 +88,32 @@ module SMARTAppLaunch
|
|
95
88
|
}
|
96
89
|
)
|
97
90
|
|
91
|
+
test from: :tls_version_test,
|
92
|
+
id: :standalone_auth_tls,
|
93
|
+
title: 'OAuth 2.0 authorize endpoint secured by transport layer security',
|
94
|
+
description: %(
|
95
|
+
Apps MUST assure that sensitive information (authentication secrets,
|
96
|
+
authorization codes, tokens) is transmitted ONLY to authenticated
|
97
|
+
servers, over TLS-secured channels.
|
98
|
+
),
|
99
|
+
config: {
|
100
|
+
inputs: { url: { name: :smart_authorization_url } },
|
101
|
+
options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
|
102
|
+
}
|
98
103
|
test from: :smart_app_redirect
|
99
104
|
test from: :smart_code_received
|
105
|
+
test from: :tls_version_test,
|
106
|
+
id: :standalone_token_tls,
|
107
|
+
title: 'OAuth 2.0 token endpoint secured by transport layer security',
|
108
|
+
description: %(
|
109
|
+
Apps MUST assure that sensitive information (authentication secrets,
|
110
|
+
authorization codes, tokens) is transmitted ONLY to authenticated
|
111
|
+
servers, over TLS-secured channels.
|
112
|
+
),
|
113
|
+
config: {
|
114
|
+
inputs: { url: { name: :smart_token_url } },
|
115
|
+
options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
|
116
|
+
}
|
100
117
|
test from: :smart_token_exchange
|
101
118
|
test from: :smart_token_response_body
|
102
119
|
test from: :smart_token_response_headers
|
@@ -14,11 +14,29 @@ module SMARTAppLaunch
|
|
14
14
|
:smart_token_url,
|
15
15
|
:client_id
|
16
16
|
input :client_secret, optional: true
|
17
|
+
input :use_pkce,
|
18
|
+
title: 'Proof Key for Code Exchange (PKCE)',
|
19
|
+
type: 'radio',
|
20
|
+
default: 'false',
|
21
|
+
options: {
|
22
|
+
list_options: [
|
23
|
+
{
|
24
|
+
label: 'Enabled',
|
25
|
+
value: 'true'
|
26
|
+
},
|
27
|
+
{
|
28
|
+
label: 'Disabled',
|
29
|
+
value: 'false'
|
30
|
+
}
|
31
|
+
]
|
32
|
+
}
|
33
|
+
input :pkce_code_verifier, optional: true
|
17
34
|
output :token_retrieval_time
|
35
|
+
output :smart_credentials
|
18
36
|
uses_request :redirect
|
19
37
|
makes_request :token
|
20
38
|
|
21
|
-
config options: { redirect_uri: "#{Inferno::Application['
|
39
|
+
config options: { redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect" }
|
22
40
|
|
23
41
|
run do
|
24
42
|
skip_if request.query_parameters['error'].present?, 'Error during authorization request'
|
@@ -37,11 +55,27 @@ module SMARTAppLaunch
|
|
37
55
|
oauth2_params[:client_id] = client_id
|
38
56
|
end
|
39
57
|
|
58
|
+
if use_pkce == 'true'
|
59
|
+
oauth2_params[:code_verifier] = pkce_code_verifier
|
60
|
+
end
|
61
|
+
|
40
62
|
post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
|
41
63
|
|
64
|
+
assert_response_status(200)
|
65
|
+
assert_valid_json(request.response_body)
|
66
|
+
|
42
67
|
output token_retrieval_time: Time.now.iso8601
|
43
68
|
|
44
|
-
|
69
|
+
token_response_body = JSON.parse(request.response_body)
|
70
|
+
output smart_credentials: {
|
71
|
+
refresh_token: token_response_body['refresh_token'],
|
72
|
+
access_token: token_response_body['access_token'],
|
73
|
+
expires_in: token_response_body['expires_in'],
|
74
|
+
client_id: client_id,
|
75
|
+
client_secret: client_secret,
|
76
|
+
token_retrieval_time: token_retrieval_time,
|
77
|
+
token_url: smart_token_url
|
78
|
+
}.to_json
|
45
79
|
end
|
46
80
|
end
|
47
81
|
end
|
@@ -5,21 +5,12 @@ module SMARTAppLaunch
|
|
5
5
|
include TokenPayloadValidation
|
6
6
|
|
7
7
|
id :smart_token_refresh_body
|
8
|
-
title '
|
8
|
+
title 'Token refresh response contains all required fields'
|
9
9
|
description %(
|
10
|
-
Server successfully exchanges refresh token at OAuth token endpoint
|
11
|
-
without providing scope in the body of the request.
|
12
|
-
|
13
10
|
The EHR authorization server SHALL return a JSON structure that includes
|
14
11
|
an access token or a message indicating that the authorization request
|
15
12
|
has been denied. `access_token`, `expires_in`, `token_type`, and `scope` are
|
16
13
|
required. `access_token` must be `Bearer`.
|
17
|
-
|
18
|
-
Although not required in the token refresh portion of the SMART App
|
19
|
-
Launch Guide, the token refresh response should include the HTTP
|
20
|
-
Cache-Control response header field with a value of no-store, as well as
|
21
|
-
the Pragma response header field with a value of no-cache to be
|
22
|
-
consistent with the requirements of the inital access token exchange.
|
23
14
|
)
|
24
15
|
input :received_scopes
|
25
16
|
output :refresh_token, :access_token, :token_retrieval_time, :expires_in, :received_scopes
|
@@ -10,11 +10,6 @@ module SMARTAppLaunch
|
|
10
10
|
Server successfully exchanges refresh token at OAuth token endpoint
|
11
11
|
without providing scope in the body of the request.
|
12
12
|
|
13
|
-
The EHR authorization server SHALL return a JSON structure that includes
|
14
|
-
an access token or a message indicating that the authorization request
|
15
|
-
has been denied. `access_token`, `expires_in`, `token_type`, and `scope` are
|
16
|
-
required. `access_token` must be `Bearer`.
|
17
|
-
|
18
13
|
Although not required in the token refresh portion of the SMART App
|
19
14
|
Launch Guide, the token refresh response should include the HTTP
|
20
15
|
Cache-Control response header field with a value of no-store, as well as
|
@@ -23,6 +18,7 @@ module SMARTAppLaunch
|
|
23
18
|
)
|
24
19
|
input :well_known_token_url, :refresh_token, :client_id, :received_scopes
|
25
20
|
input :client_secret, optional: true
|
21
|
+
output :smart_credentials, :token_retrieval_time
|
26
22
|
makes_request :token_refresh
|
27
23
|
|
28
24
|
run do
|
@@ -46,7 +42,20 @@ module SMARTAppLaunch
|
|
46
42
|
post(well_known_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
|
47
43
|
|
48
44
|
assert_response_status(200)
|
49
|
-
assert_valid_json(
|
45
|
+
assert_valid_json(request.response_body)
|
46
|
+
|
47
|
+
output token_retrieval_time: Time.now.iso8601
|
48
|
+
|
49
|
+
token_response_body = JSON.parse(request.response_body)
|
50
|
+
output smart_credentials: {
|
51
|
+
refresh_token: token_response_body['refresh_token'],
|
52
|
+
access_token: token_response_body['access_token'],
|
53
|
+
expires_in: token_response_body['expires_in'],
|
54
|
+
client_id: client_id,
|
55
|
+
client_secret: client_secret,
|
56
|
+
token_retrieval_time: token_retrieval_time,
|
57
|
+
token_url: well_known_token_url
|
58
|
+
}.to_json
|
50
59
|
end
|
51
60
|
end
|
52
61
|
end
|
@@ -1,13 +1,36 @@
|
|
1
|
+
require 'tls_test_kit'
|
2
|
+
|
3
|
+
require_relative 'smart_app_launch/version'
|
1
4
|
require_relative 'smart_app_launch/discovery_group'
|
2
5
|
require_relative 'smart_app_launch/standalone_launch_group'
|
3
6
|
require_relative 'smart_app_launch/ehr_launch_group'
|
4
7
|
require_relative 'smart_app_launch/openid_connect_group'
|
5
8
|
require_relative 'smart_app_launch/token_refresh_group'
|
6
9
|
|
10
|
+
# TODO: Remove once this functionality is released in core:
|
11
|
+
# https://github.com/inferno-framework/inferno-core/pull/86
|
12
|
+
module Inferno
|
13
|
+
module DSL
|
14
|
+
module Runnable
|
15
|
+
def required_inputs(prior_outputs = [])
|
16
|
+
required_inputs =
|
17
|
+
inputs
|
18
|
+
.reject { |input| input_definitions[input][:optional] }
|
19
|
+
.map { |input| config.input_name(input) }
|
20
|
+
.reject { |input| prior_outputs.include?(input) }
|
21
|
+
children_required_inputs = children.flat_map { |child| child.required_inputs(prior_outputs) }
|
22
|
+
prior_outputs.concat(outputs.map { |output| config.output_name(output) })
|
23
|
+
(required_inputs + children_required_inputs).flatten.uniq
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
7
29
|
module SMARTAppLaunch
|
8
30
|
class SMARTSuite < Inferno::TestSuite
|
9
31
|
id 'smart'
|
10
|
-
title 'SMART'
|
32
|
+
title 'SMART App Launch STU1'
|
33
|
+
version VERSION
|
11
34
|
|
12
35
|
resume_test_route :get, '/launch' do
|
13
36
|
request.query_parameters['iss']
|
@@ -18,12 +41,13 @@ module SMARTAppLaunch
|
|
18
41
|
end
|
19
42
|
|
20
43
|
config options: {
|
21
|
-
redirect_uri: "#{Inferno::Application['
|
22
|
-
launch_uri: "#{Inferno::Application['
|
44
|
+
redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect",
|
45
|
+
launch_uri: "#{Inferno::Application['base_url']}/custom/smart/launch"
|
23
46
|
}
|
24
47
|
|
25
48
|
group do
|
26
49
|
title 'Standalone Launch'
|
50
|
+
id :smart_full_standalone_launch
|
27
51
|
|
28
52
|
run_as_group
|
29
53
|
|
@@ -36,7 +60,9 @@ module SMARTAppLaunch
|
|
36
60
|
inputs: {
|
37
61
|
id_token: { name: :standalone_id_token },
|
38
62
|
client_id: { name: :standalone_client_id },
|
39
|
-
requested_scopes: { name: :standalone_requested_scopes }
|
63
|
+
requested_scopes: { name: :standalone_requested_scopes },
|
64
|
+
access_token: { name: :standalone_access_token },
|
65
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
40
66
|
}
|
41
67
|
}
|
42
68
|
|
@@ -55,7 +81,8 @@ module SMARTAppLaunch
|
|
55
81
|
received_scopes: { name: :standalone_received_scopes },
|
56
82
|
access_token: { name: :standalone_access_token },
|
57
83
|
token_retrieval_time: { name: :standalone_token_retrieval_time },
|
58
|
-
expires_in: { name: :standalone_expires_in }
|
84
|
+
expires_in: { name: :standalone_expires_in },
|
85
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
59
86
|
}
|
60
87
|
}
|
61
88
|
|
@@ -75,13 +102,15 @@ module SMARTAppLaunch
|
|
75
102
|
received_scopes: { name: :standalone_received_scopes },
|
76
103
|
access_token: { name: :standalone_access_token },
|
77
104
|
token_retrieval_time: { name: :standalone_token_retrieval_time },
|
78
|
-
expires_in: { name: :standalone_expires_in }
|
105
|
+
expires_in: { name: :standalone_expires_in },
|
106
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
79
107
|
}
|
80
108
|
}
|
81
109
|
end
|
82
110
|
|
83
111
|
group do
|
84
112
|
title 'EHR Launch'
|
113
|
+
id :smart_full_ehr_launch
|
85
114
|
|
86
115
|
run_as_group
|
87
116
|
|
@@ -94,7 +123,9 @@ module SMARTAppLaunch
|
|
94
123
|
inputs: {
|
95
124
|
id_token: { name: :ehr_id_token },
|
96
125
|
client_id: { name: :ehr_client_id },
|
97
|
-
requested_scopes: { name: :
|
126
|
+
requested_scopes: { name: :ehr_requested_scopes },
|
127
|
+
access_token: { name: :ehr_access_token },
|
128
|
+
smart_credentials: { name: :ehr_smart_credentials }
|
98
129
|
}
|
99
130
|
}
|
100
131
|
|
@@ -113,7 +144,8 @@ module SMARTAppLaunch
|
|
113
144
|
received_scopes: { name: :ehr_received_scopes },
|
114
145
|
access_token: { name: :ehr_access_token },
|
115
146
|
token_retrieval_time: { name: :ehr_token_retrieval_time },
|
116
|
-
expires_in: { name: :ehr_expires_in }
|
147
|
+
expires_in: { name: :ehr_expires_in },
|
148
|
+
smart_credentials: { name: :ehr_smart_credentials }
|
117
149
|
}
|
118
150
|
}
|
119
151
|
|
@@ -133,7 +165,8 @@ module SMARTAppLaunch
|
|
133
165
|
received_scopes: { name: :ehr_received_scopes },
|
134
166
|
access_token: { name: :ehr_access_token },
|
135
167
|
token_retrieval_time: { name: :ehr_token_retrieval_time },
|
136
|
-
expires_in: { name: :ehr_expires_in }
|
168
|
+
expires_in: { name: :ehr_expires_in },
|
169
|
+
smart_credentials: { name: :ehr_smart_credentials }
|
137
170
|
}
|
138
171
|
}
|
139
172
|
end
|
metadata
CHANGED
@@ -1,43 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_app_launch_test_kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen MacVicar
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inferno_core
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.1.3
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.1.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: jwt
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2.2
|
33
|
+
version: '2.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2.2
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tls_test_kit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: database_cleaner-sequel
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,13 +153,14 @@ files:
|
|
139
153
|
- lib/smart_app_launch/token_refresh_test.rb
|
140
154
|
- lib/smart_app_launch/token_response_body_test.rb
|
141
155
|
- lib/smart_app_launch/token_response_headers_test.rb
|
156
|
+
- lib/smart_app_launch/version.rb
|
142
157
|
- lib/smart_app_launch_test_kit.rb
|
143
|
-
homepage: https://github.com/
|
158
|
+
homepage: https://github.com/inferno_framework/smart-app-launch-test-kit
|
144
159
|
licenses:
|
145
160
|
- Apache-2.0
|
146
161
|
metadata:
|
147
|
-
homepage_uri: https://github.com/
|
148
|
-
source_code_uri: https://github.com/
|
162
|
+
homepage_uri: https://github.com/inferno_framework/smart-app-launch-test-kit
|
163
|
+
source_code_uri: https://github.com/inferno_framework/smart-app-launch-test-kit
|
149
164
|
post_install_message:
|
150
165
|
rdoc_options: []
|
151
166
|
require_paths:
|