smart_app_launch_test_kit 0.0.1 → 0.1.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/lib/smart_app_launch/app_launch_test.rb +1 -1
- data/lib/smart_app_launch/app_redirect_test.rb +73 -9
- data/lib/smart_app_launch/discovery_group.rb +4 -4
- data/lib/smart_app_launch/ehr_launch_group.rb +35 -19
- data/lib/smart_app_launch/openid_connect_group.rb +5 -29
- data/lib/smart_app_launch/openid_fhir_user_claim_test.rb +24 -7
- data/lib/smart_app_launch/openid_required_configuration_fields_test.rb +1 -1
- data/lib/smart_app_launch/openid_token_header_test.rb +2 -2
- data/lib/smart_app_launch/standalone_launch_group.rb +37 -20
- data/lib/smart_app_launch/token_exchange_test.rb +36 -2
- data/lib/smart_app_launch/token_payload_validation.rb +6 -0
- data/lib/smart_app_launch/token_refresh_body_test.rb +3 -11
- data/lib/smart_app_launch/token_refresh_group.rb +4 -3
- 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: 4b5b5d83f2f6bd20c159c0c258b0a67123880f3f526c21487795c0b7e60dde44
|
4
|
+
data.tar.gz: bcd5875e3e0a8a34a5d573d1cfda6b6babfc55c1b4364834a761c21c264ad83b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76d53cd6f4b40e128dc3510f0e4e01de66e8e61c475c7db1516bf726e8f2890c4ebc08e5838e2d3714b7791a3e5a65fc4165eeae6136bada32e23a08ada64caf
|
7
|
+
data.tar.gz: 6d6a3851b0d01ceb6d1576be40eb74be5a8b86089d705c26ab877251a843d68d9d4afc10c13de43d5eb122e9e971c2e5d76f8afb17b30f3def60370769ecd660
|
@@ -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,11 +8,60 @@ 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
|
18
67
|
assert_valid_http_uri(
|
@@ -28,10 +77,29 @@ module SMARTAppLaunch
|
|
28
77
|
'redirect_uri' => config.options[:redirect_uri],
|
29
78
|
'scope' => requested_scopes,
|
30
79
|
'state' => state,
|
31
|
-
'aud' =>
|
80
|
+
'aud' => aud
|
32
81
|
}
|
33
82
|
|
34
|
-
|
83
|
+
if config.options[:launch]
|
84
|
+
oauth2_params['launch'] = config.options[:launch]
|
85
|
+
elsif self.class.inputs.include?(:launch)
|
86
|
+
oauth2_params['launch'] = launch
|
87
|
+
end
|
88
|
+
|
89
|
+
if use_pkce == 'true'
|
90
|
+
# code verifier must be between 43 and 128 characters
|
91
|
+
code_verifier = SecureRandom.uuid + '-' + SecureRandom.uuid
|
92
|
+
code_challenge =
|
93
|
+
if pkce_code_challenge_method == 'S256'
|
94
|
+
self.class.calculate_s256_challenge(code_verifier)
|
95
|
+
else
|
96
|
+
code_verifier
|
97
|
+
end
|
98
|
+
|
99
|
+
output pkce_code_verifier: code_verifier, pkce_code_challenge: code_challenge
|
100
|
+
|
101
|
+
oauth2_params.merge!('code_challenge' => code_challenge, 'code_challenge_method' => pkce_code_challenge_method)
|
102
|
+
end
|
35
103
|
|
36
104
|
authorization_url = smart_authorization_url
|
37
105
|
|
@@ -50,11 +118,7 @@ module SMARTAppLaunch
|
|
50
118
|
|
51
119
|
wait(
|
52
120
|
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
|
-
)
|
121
|
+
message: wait_message(authorization_url)
|
58
122
|
)
|
59
123
|
end
|
60
124
|
end
|
@@ -2,12 +2,13 @@ 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
|
|
8
9
|
The #{title} Sequence test looks for authorization endpoints and SMART
|
9
10
|
capabilities as described by the [SMART App Launch
|
10
|
-
Framework](
|
11
|
+
Framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html).
|
11
12
|
The SMART launch framework uses OAuth 2.0 to *authorize* apps, like
|
12
13
|
Inferno, to access certain information on a FHIR server. The
|
13
14
|
authorization service accessed at the endpoint allows users to give
|
@@ -30,7 +31,7 @@ module SMARTAppLaunch
|
|
30
31
|
|
31
32
|
For more information see:
|
32
33
|
|
33
|
-
* [SMART App Launch Framework](
|
34
|
+
* [SMART App Launch Framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html)
|
34
35
|
* [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749)
|
35
36
|
* [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
|
36
37
|
)
|
@@ -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,12 +10,13 @@ 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
|
16
17
|
|
17
18
|
The [EHR
|
18
|
-
Launch](
|
19
|
+
Launch](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#ehr-launch-sequence)
|
19
20
|
is one of two ways in which an app can be launched, the other being
|
20
21
|
Standalone launch. In an EHR launch, the app is launched from an
|
21
22
|
existing EHR session or portal by a redirect to the registered launch
|
@@ -34,7 +35,7 @@ module SMARTAppLaunch
|
|
34
35
|
|
35
36
|
For more information on the #{title} see:
|
36
37
|
|
37
|
-
* [SMART EHR Launch Sequence](
|
38
|
+
* [SMART EHR Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#ehr-launch-sequence)
|
38
39
|
)
|
39
40
|
|
40
41
|
config(
|
@@ -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,13 +11,14 @@ 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
|
17
18
|
|
18
19
|
OpenID Connect (OIDC) provides the ability to verify the identity of the
|
19
20
|
authorizing user. Within the [SMART App Launch
|
20
|
-
Framework](
|
21
|
+
Framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html), Applications can
|
21
22
|
request an `id_token` be provided with by including the `openid fhirUser`
|
22
23
|
scopes when requesting authorization.
|
23
24
|
|
@@ -35,9 +36,9 @@ module SMARTAppLaunch
|
|
35
36
|
|
36
37
|
For more information see:
|
37
38
|
|
38
|
-
* [SMART App Launch Framework](
|
39
|
-
* [Scopes for requesting identity data](
|
40
|
-
* [Apps Requesting Authorization](
|
39
|
+
* [SMART App Launch Framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html)
|
40
|
+
* [Scopes for requesting identity data](https://www.hl7.org/fhir/smart-app-launch/1.0.0/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
|
41
|
+
* [Apps Requesting Authorization](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#step-1-app-asks-for-authorization)
|
41
42
|
* [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
|
42
43
|
)
|
43
44
|
|
@@ -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
|
@@ -9,7 +9,7 @@ module SMARTAppLaunch
|
|
9
9
|
`id_token_signing_alg_values_supported`.
|
10
10
|
|
11
11
|
Additionally, the [SMART App Launch
|
12
|
-
Framework](
|
12
|
+
Framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
|
13
13
|
requires that the RSA SHA-256 signing algorithm be supported.
|
14
14
|
)
|
15
15
|
|
@@ -3,9 +3,9 @@ module SMARTAppLaunch
|
|
3
3
|
id :smart_openid_token_header
|
4
4
|
title 'ID token header contains required information'
|
5
5
|
description %(
|
6
|
-
Verify that the id token header indicates that the
|
6
|
+
Verify that the id token header indicates that the token is signed using
|
7
7
|
RSA SHA-256 [as required by the SMART app launch
|
8
|
-
framework](
|
8
|
+
framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
|
9
9
|
and that the key used to sign the token can be identified in the JWKS.
|
10
10
|
)
|
11
11
|
|
@@ -8,13 +8,14 @@ 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
|
14
15
|
|
15
16
|
The [Standalone
|
16
|
-
Launch](
|
17
|
-
|
17
|
+
Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
|
18
|
+
allows an app, like Inferno, to be launched independent of an
|
18
19
|
existing EHR session. It is one of the two launch methods described in
|
19
20
|
the SMART App Launch Framework alongside EHR Launch. The app will
|
20
21
|
request authorization for the provided scope from the authorization
|
@@ -30,7 +31,7 @@ module SMARTAppLaunch
|
|
30
31
|
|
31
32
|
For more information on the #{title}:
|
32
33
|
|
33
|
-
* [Standalone Launch Sequence](
|
34
|
+
* [Standalone Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
|
34
35
|
)
|
35
36
|
|
36
37
|
config(
|
@@ -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
|
@@ -28,6 +28,12 @@ module SMARTAppLaunch
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
def validate_scope_subset(received_scopes, original_scopes)
|
32
|
+
extra_scopes = received_scopes.split - original_scopes.split
|
33
|
+
assert extra_scopes.empty?, "Token response contained scopes which are not a subset of the scope granted to the "\
|
34
|
+
"original access token: #{extra_scopes.join(', ')}"
|
35
|
+
end
|
36
|
+
|
31
37
|
def validate_token_field_types(body)
|
32
38
|
STRING_FIELDS
|
33
39
|
.select { |field| body[field].present? }
|
@@ -5,21 +5,14 @@ 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
14
|
|
18
|
-
|
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.
|
15
|
+
Scopes returned must be a strict subset of the scopes granted in the original launch.
|
23
16
|
)
|
24
17
|
input :received_scopes
|
25
18
|
output :refresh_token, :access_token, :token_retrieval_time, :expires_in, :received_scopes
|
@@ -45,8 +38,7 @@ module SMARTAppLaunch
|
|
45
38
|
validate_token_field_types(body)
|
46
39
|
validate_token_type(body)
|
47
40
|
|
48
|
-
|
49
|
-
'Received scopes not equal to originally granted scopes'
|
41
|
+
validate_scope_subset(received_scopes, old_received_scopes)
|
50
42
|
end
|
51
43
|
end
|
52
44
|
end
|
@@ -6,10 +6,11 @@ module SMARTAppLaunch
|
|
6
6
|
class TokenRefreshGroup < Inferno::TestGroup
|
7
7
|
id :smart_token_refresh
|
8
8
|
title 'SMART Token Refresh'
|
9
|
+
short_description 'Demonstrate the ability to exchange a refresh token for an access token.'
|
9
10
|
description %(
|
10
11
|
# Background
|
11
12
|
|
12
|
-
The #{title} Sequence tests the ability of the system to
|
13
|
+
The #{title} Sequence tests the ability of the system to successfully
|
13
14
|
exchange a refresh token for an access token. Refresh tokens are typically
|
14
15
|
longer lived than access tokens and allow client applications to obtain a
|
15
16
|
new access token Refresh tokens themselves cannot provide access to
|
@@ -17,7 +18,7 @@ module SMARTAppLaunch
|
|
17
18
|
|
18
19
|
Token refreshes are accomplished through a `POST` request to the token
|
19
20
|
exchange endpoint as described in the [SMART App Launch
|
20
|
-
Framework](
|
21
|
+
Framework](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token).
|
21
22
|
|
22
23
|
# Test Methodology
|
23
24
|
|
@@ -30,7 +31,7 @@ module SMARTAppLaunch
|
|
30
31
|
* [The OAuth 2.0 Authorization
|
31
32
|
Framework](https://tools.ietf.org/html/rfc6749)
|
32
33
|
* [Using a refresh token to obtain a new access
|
33
|
-
token](
|
34
|
+
token](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token)
|
34
35
|
)
|
35
36
|
|
36
37
|
test from: :smart_token_refresh
|
@@ -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.
|
4
|
+
version: 0.1.2
|
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-04-07 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:
|