smart_app_launch_test_kit 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|