smart_app_launch_test_kit 0.0.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae741dbca6b676c7c53c6be41a14f84292b4d620dbfecbf67f7472f5450c67fc
4
- data.tar.gz: 4136c596fa810d8109c18df863e39eda7283c170a59c7441dc07e1bcb4ebdc1f
3
+ metadata.gz: 4b5b5d83f2f6bd20c159c0c258b0a67123880f3f526c21487795c0b7e60dde44
4
+ data.tar.gz: bcd5875e3e0a8a34a5d573d1cfda6b6babfc55c1b4364834a761c21c264ad83b
5
5
  SHA512:
6
- metadata.gz: dfa9bd45de2c2b917347724b9a706dde71d1287dc48a3e49a86371b1106c42257084f78f989dcb737fba425594fbc11f4562b3aab7bf37710a40cdff3fccb116
7
- data.tar.gz: ce2e0ed22fd192d88c5de32f334ada292ee5813b72c184eb9161f122d2cec82155ff6e83de19f8fe2c2d7a2ea0cd52b5c4adcdcefef0a668ff079abb7107776d
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['inferno_host']}/custom/smart/launch" }
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['inferno_host']}/custom/smart/redirect" }
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' => url
80
+ 'aud' => aud
32
81
  }
33
82
 
34
- oauth2_params['launch'] = launch if self.class.inputs.include?(:launch)
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](http://hl7.org/fhir/smart-app-launch/conformance/index.html).
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](http://hl7.org/fhir/smart-app-launch/index.html)
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](http://hl7.org/fhir/smart-app-launch/index.html#ehr-launch-sequence)
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](http://hl7.org/fhir/smart-app-launch/index.html#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](http://hl7.org/fhir/smart-app-launch/), Applications can
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](http://hl7.org/fhir/smart-app-launch/)
39
- * [Scopes for requesting identity data](http://hl7.org/fhir/smart-app-launch/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
40
- * [Apps Requesting Authorization](http://hl7.org/fhir/smart-app-launch/#step-1-app-asks-for-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 'ID token contains a valid fhirUser claim'
4
+ title 'FHIR resource representing the current user can be retrieved'
5
5
  description %(
6
- Verify that the `fhirUser` claim is present in the ID token. The
7
- `fhirUser` claim must be the url for a Patient, Practitioner,
8
- RelatedPerson, or Person resource.
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
- assert valid_fhir_user_resource_types.any? { |type| fhir_user.include? type },
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](http://www.hl7.org/fhir/smart-app-launch/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
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 tokenis signed using
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](http://www.hl7.org/fhir/smart-app-launch/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
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](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
17
- Sequence allows an app, like Inferno, to be launched independent of an
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](http://hl7.org/fhir/smart-app-launch/#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['inferno_host']}/custom/smart/redirect" }
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
- assert_response_status(200)
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 'Server successfully refreshes the access token when optional scope parameter omitted'
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
- 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.
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
- assert received_scopes.split.sort == old_received_scopes.split.sort,
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 successfuly
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](http://www.hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token).
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](http://hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-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(response[:body])
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
@@ -0,0 +1,3 @@
1
+ module SMARTAppLaunch
2
+ VERSION = '0.1.2'
3
+ 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['inferno_host']}/custom/smart/redirect",
22
- launch_uri: "#{Inferno::Application['inferno_host']}/custom/smart/launch"
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: :standalone_requested_scopes }
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.1
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: 2021-10-21 00:00:00.000000000 Z
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.0.6
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.0.6
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.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.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/inferno_community/smart-app-launch-test-kit
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/inferno_community/smart-app-launch-test-kit
148
- source_code_uri: https://github.com/inferno_community/smart-app-launch-test-kit
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: