smart_app_launch_test_kit 0.4.4 → 0.4.6

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: 1565daad4304e65881d1a3a373d29c8674d19d57bf46342a1cdc95db6c29b6f4
4
- data.tar.gz: 4923648577e4d53631a8efa7ccae1f254957d490e579359415ca4f9d426ac5fe
3
+ metadata.gz: af89139a6750969a3efa3731229c4ff2a8754738f75024b10aa986cb69325dbb
4
+ data.tar.gz: 8b412a1bb5601e932bc149f0de5e5dc59aaf5a7dc5be962a98a575a378fb297e
5
5
  SHA512:
6
- metadata.gz: c1266d06f6b1bd55a93e84b93cdd0de9d628ac5d5eb084dc38817e5639869a35ccb2f5173163f0966786937ef3821a5b1e317233af2be5e042afca840f4649c3
7
- data.tar.gz: 5101dabbab6be10be047d9e378f43fc939d605f7adcb5277c07c738f2aabbf80e5b565cb1a1885ed35184ac5b5bdf2b9a4311396498bb02766ed925986ce2433
6
+ metadata.gz: c67c07a022b652ab979b1d5f1d857d2ffb44b63030e00f11f0d1ed5b0cede5a1371abef7263ea8e6c517acb617c33e3698bf74686fdec407eac5d37a48ceedc6
7
+ data.tar.gz: cef63d441dfd3f5b1cc2d0c902bab6632006f447b494ba39ebbb2a4c8b87d82cf457c60f0108a22f752c5ac525a94b69d962622b234901805aaf72a8f750e53f
@@ -0,0 +1,40 @@
1
+ require_relative 'url_helpers'
2
+
3
+ module SMARTAppLaunch
4
+ class CORSMetadataRequest < Inferno::Test
5
+ id :smart_cors_metadata_request
6
+
7
+ include URLHelpers
8
+
9
+ title 'SMART metadata Endpoint Enables Cross-Origin Resource Sharing (CORS)'
10
+ description %(
11
+ The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
12
+ specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
13
+ as follows:
14
+
15
+ - For requests from any origin, CORS configuration permits access to the public discovery endpoints
16
+ (.well-known/smart-configuration and metadata).
17
+
18
+ This test verifies that the metadata request is returned with the appropriate CORS header.
19
+ )
20
+ optional
21
+
22
+ input :url
23
+
24
+ fhir_client do
25
+ url :url
26
+ headers 'Origin' => Inferno::Application['inferno_host']
27
+ end
28
+
29
+ run do
30
+ fhir_get_capability_statement
31
+
32
+ assert_response_status(200)
33
+ inferno_origin = Inferno::Application['inferno_host']
34
+ cors_allow_origin = request.response_header('Access-Control-Allow-Origin')&.value
35
+ assert cors_allow_origin.present?, 'No `Access-Control-Allow-Origin` header received.'
36
+ assert cors_allow_origin == inferno_origin || cors_allow_origin == '*',
37
+ "`Access-Control-Allow-Origin` must be `#{inferno_origin}`, but received: `#{cors_allow_origin}`"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,48 @@
1
+ module SMARTAppLaunch
2
+ class CORSOpenIDFHIRUserClaimTest < Inferno::Test
3
+ id :smart_cors_openid_fhir_user_claim
4
+ title 'SMART FHIR User REST API Endpoint Enables Cross-Origin Resource Sharing (CORS)'
5
+ description %(
6
+ The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
7
+ specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
8
+ as follows:
9
+
10
+ - For requests from a client's registered origin(s), CORS configuration permits access to the token
11
+ endpoint and to FHIR REST API endpoints.
12
+
13
+ This test verifies that a request to the FHIR REST API endpoint for the FHIR user is returned with the appropriate
14
+ CORS header.
15
+ )
16
+ optional
17
+
18
+ input :url, :id_token_fhir_user
19
+ input :smart_credentials, type: :oauth_credentials
20
+
21
+ fhir_client do
22
+ url :url
23
+ oauth_credentials :smart_credentials
24
+ headers 'Origin' => Inferno::Application['inferno_host']
25
+ end
26
+
27
+ run do
28
+ valid_fhir_user_resource_types = ['Patient', 'Practitioner', 'RelatedPerson', 'Person']
29
+
30
+ fhir_user_segments = id_token_fhir_user.split('/')
31
+ fhir_user_resource_type = fhir_user_segments[-2]
32
+ fhir_user_id = fhir_user_segments.last
33
+
34
+ assert valid_fhir_user_resource_types.include?(fhir_user_resource_type),
35
+ "ID token `fhirUser` claim does not refer to a valid resource type: #{id_token_fhir_user}"
36
+
37
+ fhir_read(fhir_user_resource_type, fhir_user_id)
38
+
39
+ assert_response_status(200)
40
+
41
+ inferno_origin = Inferno::Application['inferno_host']
42
+ cors_allow_origin = request.response_header('Access-Control-Allow-Origin')&.value
43
+ assert cors_allow_origin.present?, 'No `Access-Control-Allow-Origin` header received.'
44
+ assert cors_allow_origin == inferno_origin || cors_allow_origin == '*',
45
+ "`Access-Control-Allow-Origin` must be `#{inferno_origin}`, but received: `#{cors_allow_origin}`"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ module SMARTAppLaunch
2
+ class CORSTokenExchangeTest < Inferno::Test
3
+ title 'SMART Token Endpoint Enables Cross-Origin Resource Sharing (CORS)'
4
+ description %(
5
+ The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
6
+ specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
7
+ as follows:
8
+
9
+ - For requests from a client's registered origin(s), CORS configuration permits access to the token
10
+ endpoint
11
+
12
+ This test verifies that the token endpoint contains the appropriate CORS header in the response.
13
+ )
14
+ id :smart_cors_token_exchange
15
+
16
+ uses_request :cors_token_request
17
+
18
+ input :client_auth_type
19
+
20
+ run do
21
+ omit_if client_auth_type != 'public', %(
22
+ Client type is not public, Cross-Origin Resource Sharing (CORS) is not required to be supported for
23
+ non-public client types
24
+ )
25
+
26
+ skip_if request.status != 200, 'Previous request was unsuccessful, cannot check for CORS support'
27
+
28
+ inferno_origin = Inferno::Application['inferno_host']
29
+ cors_header = request.response_header('Access-Control-Allow-Origin')&.value
30
+
31
+ assert cors_header == inferno_origin || cors_header == '*',
32
+ "Request must have `Access-Control-Allow-Origin` header containing `#{inferno_origin}`"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'url_helpers'
2
+
3
+ module SMARTAppLaunch
4
+ class CORSWellKnownEndpointTest < Inferno::Test
5
+ include URLHelpers
6
+
7
+ title 'SMART .well-known/smart-configuration Endpoint Enables Cross-Origin Resource Sharing (CORS)'
8
+ id :smart_cors_well_known_endpoint
9
+ description %(
10
+ The SMART [Considerations for Cross-Origin Resource Sharing (CORS) support](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support)
11
+ specifies that servers that support purely browser-based apps SHALL enable Cross-Origin Resource Sharing (CORS)
12
+ as follows:
13
+
14
+ - For requests from any origin, CORS configuration permits access to the public discovery endpoints
15
+ (.well-known/smart-configuration and metadata).
16
+
17
+ This test verifies that the .well-known/smart-configuration request is returned with the appropriate CORS header.
18
+ )
19
+ optional
20
+
21
+ input :url,
22
+ title: 'FHIR Endpoint',
23
+ description: 'URL of the FHIR endpoint used by SMART applications'
24
+
25
+ run do
26
+ well_known_configuration_url = "#{url.chomp('/')}/.well-known/smart-configuration"
27
+ inferno_origin = Inferno::Application['inferno_host']
28
+
29
+ get(well_known_configuration_url,
30
+ headers: { 'Accept' => 'application/json',
31
+ 'Origin' => inferno_origin })
32
+ assert_response_status(200)
33
+
34
+ cors_allow_origin = request.response_header('Access-Control-Allow-Origin')&.value
35
+ assert cors_allow_origin.present?, 'No `Access-Control-Allow-Origin` header received.'
36
+ assert cors_allow_origin == inferno_origin || cors_allow_origin == '*',
37
+ "`Access-Control-Allow-Origin` must be `#{inferno_origin}`, but received: `#{cors_allow_origin}`"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'cors_metadata_request_test'
2
+ require_relative 'cors_well_known_endpoint_test'
3
+ require_relative 'discovery_stu2_group'
4
+
5
+ module SMARTAppLaunch
6
+ class DiscoverySTU22Group < DiscoverySTU2Group
7
+ id :smart_discovery_stu2_2
8
+
9
+ test from: :smart_cors_well_known_endpoint
10
+ test from: :smart_cors_metadata_request
11
+ end
12
+ end
@@ -1,5 +1,7 @@
1
1
  require_relative 'ehr_launch_group_stu2'
2
2
  require_relative 'token_response_body_test_stu2_2'
3
+ require_relative 'cors_token_exchange_test'
4
+ require_relative 'token_exchange_stu2_2_test'
3
5
 
4
6
  module SMARTAppLaunch
5
7
  class EHRLaunchGroupSTU22 < EHRLaunchGroupSTU2
@@ -46,9 +48,21 @@ module SMARTAppLaunch
46
48
  }
47
49
  )
48
50
 
51
+ test from: :smart_token_exchange_stu2_2
52
+
53
+ token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'smart_token_exchange' }
54
+ children[token_exchange_index] = children.pop
55
+
49
56
  test from: :smart_token_response_body_stu2_2
50
57
 
51
58
  token_response_body_index = children.find_index { |child| child.id.to_s.end_with? 'token_response_body' }
52
59
  children[token_response_body_index] = children.pop
60
+
61
+ test from: :smart_cors_token_exchange,
62
+ config: {
63
+ requests: {
64
+ cors_token_request: { name: :ehr_token }
65
+ }
66
+ }
53
67
  end
54
68
  end
@@ -0,0 +1,40 @@
1
+ require_relative 'cors_openid_fhir_user_claim_test'
2
+ require_relative 'openid_connect_group'
3
+
4
+ module SMARTAppLaunch
5
+ class OpenIDConnectGroupSTU22 < OpenIDConnectGroup
6
+ id :smart_openid_connect_stu2_2
7
+ title 'OpenID Connect'
8
+ short_description 'Demonstrate the ability to authenticate users with OpenID Connect.'
9
+
10
+ description %(
11
+ # Background
12
+
13
+ OpenID Connect (OIDC) provides the ability to verify the identity of the
14
+ authorizing user. Within the [SMART App Launch
15
+ Framework](https://www.hl7.org/fhir/smart-app-launch/STU2.2/), Applications can
16
+ request an `id_token` be provided with by including the `openid fhirUser`
17
+ scopes when requesting authorization.
18
+
19
+ # Test Methodology
20
+
21
+ This sequence validates the id token returned as part of the OAuth 2.0
22
+ token response. Once the token is decoded, the server's OIDC configuration
23
+ is retrieved from its well-known configuration endpoint. This
24
+ configuration is checked to ensure that all required fields are present.
25
+ Next the keys used to cryptographically sign the id token are retrieved
26
+ from the url contained in the OIDC configuration. Then the header,
27
+ payload, and signature of the id token are validated. Finally, the FHIR
28
+ resource from the `fhirUser` claim in the id token is fetched from the
29
+ FHIR server.
30
+
31
+ For more information see:
32
+
33
+ * [SMART App Launch Framework](https://www.hl7.org/fhir/smart-app-launch/STU2.2/)
34
+ * [Scopes for requesting identity data](https://www.hl7.org/fhir/smart-app-launch/STU2.2/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
35
+ * [Apps Requesting Authorization](https://www.hl7.org/fhir/smart-app-launch/STU2.2/index.html#step-1-app-asks-for-authorization)
36
+ * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
37
+ )
38
+ test from: :smart_cors_openid_fhir_user_claim
39
+ end
40
+ end
@@ -2,12 +2,11 @@ require 'tls_test_kit'
2
2
 
3
3
  require_relative 'jwks'
4
4
  require_relative 'version'
5
- require_relative 'discovery_stu2_group'
5
+ require_relative 'discovery_stu2_2_group'
6
6
  require_relative 'standalone_launch_group_stu2_2'
7
7
  require_relative 'ehr_launch_group_stu2_2'
8
- require_relative 'openid_connect_group'
8
+ require_relative 'openid_connect_group_stu2_2'
9
9
  require_relative 'token_introspection_group_stu2_2'
10
- require_relative 'token_refresh_stu2_group'
11
10
  require_relative 'backend_services_authorization_group'
12
11
 
13
12
  module SMARTAppLaunch
@@ -55,6 +54,9 @@ module SMARTAppLaunch
55
54
  following JWK Set URL:
56
55
 
57
56
  * `#{Inferno::Application[:base_url]}/custom/smart_stu2_2/.well-known/jwks.json`
57
+
58
+ **NOTE:** This suite does not currently test [CORS
59
+ support](http://hl7.org/fhir/smart-app-launch/app-launch.html#considerations-for-cross-origin-resource-sharing-cors-support).
58
60
  DESCRIPTION
59
61
 
60
62
  input_instructions %(
@@ -89,10 +91,10 @@ module SMARTAppLaunch
89
91
 
90
92
  run_as_group
91
93
 
92
- group from: :smart_discovery_stu2
94
+ group from: :smart_discovery_stu2_2
93
95
  group from: :smart_standalone_launch_stu2_2
94
96
 
95
- group from: :smart_openid_connect,
97
+ group from: :smart_openid_connect_stu2_2,
96
98
  config: {
97
99
  inputs: {
98
100
  id_token: { name: :standalone_id_token },
@@ -164,11 +166,11 @@ module SMARTAppLaunch
164
166
 
165
167
  run_as_group
166
168
 
167
- group from: :smart_discovery_stu2
169
+ group from: :smart_discovery_stu2_2
168
170
 
169
171
  group from: :smart_ehr_launch_stu2_2
170
172
 
171
- group from: :smart_openid_connect,
173
+ group from: :smart_openid_connect_stu2_2,
172
174
  config: {
173
175
  inputs: {
174
176
  id_token: { name: :ehr_id_token },
@@ -234,7 +236,7 @@ module SMARTAppLaunch
234
236
 
235
237
  run_as_group
236
238
 
237
- group from: :smart_discovery_stu2
239
+ group from: :smart_discovery_stu2_2
238
240
  group from: :backend_services_authorization
239
241
  end
240
242
 
@@ -1,5 +1,7 @@
1
1
  require_relative 'token_response_body_test_stu2_2'
2
2
  require_relative 'standalone_launch_group_stu2'
3
+ require_relative 'cors_token_exchange_test'
4
+ require_relative 'token_exchange_stu2_2_test'
3
5
 
4
6
  module SMARTAppLaunch
5
7
  class StandaloneLaunchGroupSTU22 < StandaloneLaunchGroupSTU2
@@ -44,9 +46,21 @@ module SMARTAppLaunch
44
46
  }
45
47
  )
46
48
 
49
+ test from: :smart_token_exchange_stu2_2
50
+
51
+ token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'token_exchange' }
52
+ children[token_exchange_index] = children.pop
53
+
47
54
  test from: :smart_token_response_body_stu2_2
48
55
 
49
56
  token_response_body_index = children.find_index { |child| child.id.to_s.end_with? 'token_response_body' }
50
57
  children[token_response_body_index] = children.pop
58
+
59
+ test from: :smart_cors_token_exchange,
60
+ config: {
61
+ requests: {
62
+ cors_token_request: { name: :standalone_token }
63
+ }
64
+ }
51
65
  end
52
66
  end
@@ -0,0 +1,30 @@
1
+ require_relative 'token_exchange_stu2_test'
2
+
3
+ module SMARTAppLaunch
4
+ class TokenExchangeSTU22Test < TokenExchangeSTU2Test
5
+ id :smart_token_exchange_stu2_2
6
+
7
+ def add_credentials_to_request(oauth2_params, oauth2_headers)
8
+ if client_auth_type == 'confidential_symmetric'
9
+ assert client_secret.present?,
10
+ 'A client secret must be provided when using confidential symmetric client authentication.'
11
+
12
+ client_credentials = "#{client_id}:#{client_secret}"
13
+ oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
14
+ elsif client_auth_type == 'public'
15
+ oauth2_params[:client_id] = client_id
16
+ oauth2_headers['Origin'] = Inferno::Application['inferno_host']
17
+ else
18
+ oauth2_params.merge!(
19
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
20
+ client_assertion: ClientAssertionBuilder.build(
21
+ iss: client_id,
22
+ sub: client_id,
23
+ aud: smart_token_url,
24
+ client_auth_encryption_method:
25
+ )
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -51,7 +51,7 @@ module SMARTAppLaunch
51
51
  skip_if request.query_parameters['error'].present?, 'Error during authorization request'
52
52
 
53
53
  oauth2_params = {
54
- code: code,
54
+ code:,
55
55
  redirect_uri: config.options[:redirect_uri],
56
56
  grant_type: 'authorization_code'
57
57
  }
@@ -59,9 +59,7 @@ module SMARTAppLaunch
59
59
 
60
60
  add_credentials_to_request(oauth2_params, oauth2_headers)
61
61
 
62
- if use_pkce == 'true'
63
- oauth2_params[:code_verifier] = pkce_code_verifier
64
- end
62
+ oauth2_params[:code_verifier] = pkce_code_verifier if use_pkce == 'true'
65
63
 
66
64
  post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
67
65
 
@@ -72,17 +70,15 @@ module SMARTAppLaunch
72
70
 
73
71
  token_response_body = JSON.parse(request.response_body)
74
72
 
75
-
76
73
  output smart_credentials: {
77
- refresh_token: token_response_body['refresh_token'],
78
- access_token: token_response_body['access_token'],
79
- expires_in: token_response_body['expires_in'],
80
- client_id: client_id,
81
- client_secret: client_secret,
82
- token_retrieval_time: token_retrieval_time,
83
- token_url: smart_token_url
84
- }.to_json
85
-
74
+ refresh_token: token_response_body['refresh_token'],
75
+ access_token: token_response_body['access_token'],
76
+ expires_in: token_response_body['expires_in'],
77
+ client_id:,
78
+ client_secret:,
79
+ token_retrieval_time:,
80
+ token_url: smart_token_url
81
+ }.to_json
86
82
  end
87
83
  end
88
84
  end
@@ -1,7 +1,7 @@
1
1
  require_relative 'standalone_launch_group_stu2_2'
2
2
 
3
3
  module SMARTAppLaunch
4
- class SMARTTokenIntrospectionAccessTokenGroupSTU22 < SMARTTokenIntrospectionAccessTokenGroup
4
+ class SMARTTokenIntrospectionAccessTokenGroupSTU22 < Inferno::TestGroup
5
5
  title 'Request New Access Token to Introspect'
6
6
  run_as_group
7
7
 
@@ -20,9 +20,7 @@ module SMARTAppLaunch
20
20
  Register Inferno as a Standalone SMART App and provide the registration details below.
21
21
  )
22
22
 
23
+ group from: :smart_discovery_stu2_2
23
24
  group from: :smart_standalone_launch_stu2_2
24
-
25
- standalone_launch_index = children.find_index { |child| child.id.to_s.end_with? 'standalone_launch_stu2' }
26
- children[standalone_launch_index] = children.pop
27
25
  end
28
26
  end
@@ -57,13 +57,12 @@ module SMARTAppLaunch
57
57
 
58
58
  If the introspection endpoint is protected, testers must enter their own HTTP Authorization header for the introspection request. See
59
59
  [RFC 7616 The 'Basic' HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common
60
- approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
60
+ approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
61
61
  server to complete the introspection request.
62
62
 
63
63
  **Note:** For both the Authorization header and request parameters, user-input
64
64
  values will be sent exactly as entered and therefore the tester must
65
65
  URI-encode any appropriate values.
66
66
  )
67
-
68
67
  end
69
68
  end
@@ -43,26 +43,5 @@ module SMARTAppLaunch
43
43
 
44
44
  access_token_group_index = children.find_index { |child| child.id.to_s.end_with? 'access_token_group' }
45
45
  children[access_token_group_index] = children.pop
46
-
47
- input_order :url, :standalone_client_id, :standalone_client_secret,
48
- :authorization_method, :use_pkce, :pkce_code_challenge_method,
49
- :standalone_requested_scopes, :client_auth_encryption_method,
50
- :client_auth_type, :custom_authorization_header,
51
- :optional_introspection_request_params
52
- input_instructions %(
53
- Executing tests at this level will run all three Token Introspection groups back-to-back. If test groups need
54
- to be run independently, exit this window and select a specific test group instead.
55
-
56
- These tests are currently designed such that the token introspection URL must be present in the SMART well-known endpoint.
57
-
58
- If the introspection endpoint is protected, testers must enter their own HTTP Authorization header for the introspection request. See
59
- [RFC 7616 The 'Basic' HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common
60
- approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
61
- server to complete the introspection request.
62
-
63
- **Note:** For both the Authorization header and request parameters, user-input
64
- values will be sent exactly as entered and therefore the tester must
65
- URI-encode any appropriate values.
66
- )
67
46
  end
68
47
  end
@@ -11,42 +11,44 @@ module SMARTAppLaunch
11
11
  id :smart_token_introspection_request_group
12
12
  description %(
13
13
  This group of tests executes the token introspection requests and ensures the correct HTTP response is returned
14
- but does not validate the contents of the token introspection response.
14
+ but does not validate the contents of the token introspection response.
15
15
 
16
- If Inferno cannot reasonably be configured to be authorized to access the token introspection endpoint, these tests
16
+ If Inferno cannot reasonably be configured to be authorized to access the token introspection endpoint, these tests
17
17
  can be skipped. Instead, an out-of-band token introspection request must be completed and the response body
18
18
  manually provided as input for the Validate Introspection Response test group.
19
19
  )
20
20
 
21
21
  input_instructions %(
22
- If the Request New Access Token group was executed, the access token input will auto-populate with that token.
23
- Otherwise an active access token needs to be obtained out-of-band and input.
24
-
25
- Per [RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2), "the definition of an active token is
26
- currently dependent upon the authorization server, but this is commonly a token that has been issued by this
27
- authorization server, is not expired, has not been revoked, and is valid for use at the protected resource making
22
+ If the Request New Access Token group was executed, the access token input will auto-populate with that token.
23
+ Otherwise an active access token needs to be obtained out-of-band and input.
24
+
25
+ Per [RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2), "the definition of an active token is
26
+ currently dependent upon the authorization server, but this is commonly a token that has been issued by this
27
+ authorization server, is not expired, has not been revoked, and is valid for use at the protected resource making
28
28
  the introspection call."
29
29
 
30
30
  If the introspection endpoint is protected, testers must enter their own HTTP Authorization header for the introspection request. See
31
31
  [RFC 7616 The 'Basic' HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common
32
- approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
32
+ approach that uses client credentials. Testers may also provide any additional parameters needed for their authorization
33
33
  server to complete the introspection request.
34
34
 
35
35
  **Note:** For both the Authorization header and request parameters, user-input
36
36
  values will be sent exactly as entered and therefore the tester must URI-encode any appropriate values.
37
37
  )
38
38
 
39
- input :well_known_introspection_url,
40
- title: 'Token Introspection Endpoint URL',
39
+ input :well_known_introspection_url,
40
+ title: 'Token Introspection Endpoint URL',
41
41
  description: 'The complete URL of the token introspection endpoint.'
42
42
 
43
43
  input :custom_authorization_header,
44
- title: 'HTTP Authorization Header for Introspection Request',
44
+ title: 'Custom HTTP Headers for Introspection Request',
45
45
  type: 'textarea',
46
46
  optional: true,
47
47
  description: %(
48
- Include header name, auth scheme, and auth parameters.
49
- Ex: 'Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW'
48
+ Add custom headers for the introspection request by adding each header's name and value with a new line
49
+ between each header.
50
+ Ex:
51
+ <Header 1 Name>: <Value 1>
50
52
  )
51
53
 
52
54
  input :optional_introspection_request_params,
@@ -54,69 +56,78 @@ module SMARTAppLaunch
54
56
  type: 'textarea',
55
57
  optional: true,
56
58
  description: %(
57
- Any additional parameters to append to the request body, separated by &. Example: 'param1=abc&param2=def'
59
+ Any additional parameters to append to the request body, separated by &. Example: 'param1=abc&param2=def'
58
60
  )
59
61
 
60
62
  test do
61
63
  title 'Token introspection endpoint returns a response when provided an active token'
62
64
  description %(
63
65
  This test will execute a token introspection request for an active token and ensure a 200 status and valid JSON
64
- body are returned in the response.
66
+ body are returned in the response.
65
67
  )
66
68
 
67
- input :standalone_access_token,
69
+ input :standalone_access_token,
68
70
  title: 'Access Token',
69
71
  description: 'The access token to be introspected. MUST be active.'
70
72
 
71
-
72
73
  output :active_token_introspection_response_body
73
74
 
74
75
  run do
75
-
76
76
  # If this is being chained from an earlier test, it might be blank if not present in the well-known endpoint
77
77
  skip_if well_known_introspection_url.nil?, 'No introspection URL present in SMART well-known endpoint.'
78
78
 
79
- headers = {'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'}
79
+ headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' }
80
80
  body = "token=#{standalone_access_token}"
81
81
 
82
82
  if custom_authorization_header.present?
83
- parsed_header = custom_authorization_header.split(':', 2)
84
- assert parsed_header.length == 2, "Incorrect custom HTTP header format input, expected: '<header name>: <header value>'"
85
- headers[parsed_header[0]] = parsed_header[1].strip
83
+ custom_headers = custom_authorization_header.split("\n")
84
+ custom_headers.each do |custom_header|
85
+ parsed_header = custom_header.split(':', 2)
86
+ assert parsed_header.length == 2,
87
+ 'Incorrect custom HTTP header format input, expected: "<header name>: <header value>"'
88
+ headers[parsed_header[0]] = parsed_header[1].strip
89
+ end
86
90
  end
87
91
 
88
- if optional_introspection_request_params.present?
89
- body += "&#{optional_introspection_request_params}"
90
- end
92
+ body += "&#{optional_introspection_request_params}" if optional_introspection_request_params.present?
91
93
 
92
- post(well_known_introspection_url, body: body, headers: headers)
94
+ post(well_known_introspection_url, body:, headers:)
93
95
 
94
96
  assert_response_status(200)
95
97
  output active_token_introspection_response_body: request.response_body
96
98
  end
97
-
98
99
  end
99
100
 
100
- test do
101
+ test do
101
102
  title 'Token introspection endpoint returns a response when provided an invalid token'
102
103
  description %(
103
104
  This test will execute a token introspection request for an invalid token and ensure a 200 status and valid JSON
104
- body are returned in response.
105
+ body are returned in response.
105
106
  )
106
107
 
107
108
  output :invalid_token_introspection_response_body
108
109
  run do
109
-
110
110
  # If this is being chained from an earlier test, it might be blank if not present in the well-known endpoint
111
111
  skip_if well_known_introspection_url.nil?, 'No introspection URL present in SMART well-known endpoint.'
112
112
 
113
- headers = {'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'}
114
- body = "token=invalid_token_value"
115
- post(well_known_introspection_url, body: body, headers: headers)
116
-
113
+ headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' }
114
+ body = 'token=invalid_token_value'
115
+
116
+ if custom_authorization_header.present?
117
+ custom_headers = custom_authorization_header.split("\n")
118
+ custom_headers.each do |custom_header|
119
+ parsed_header = custom_header.split(':', 2)
120
+ assert parsed_header.length == 2,
121
+ 'Incorrect custom HTTP header format input, expected: "<header name>: <header value>"'
122
+ headers[parsed_header[0]] = parsed_header[1].strip
123
+ end
124
+ end
125
+
126
+ post(well_known_introspection_url, body:, headers:)
127
+
117
128
  assert_response_status(200)
118
129
  output invalid_token_introspection_response_body: request.response_body
119
130
  end
120
131
  end
121
132
  end
122
- end
133
+ end
@@ -30,6 +30,10 @@ module SMARTAppLaunch
30
30
  end
31
31
  end
32
32
 
33
+ def make_auth_token_request(smart_token_url, oauth2_params, oauth2_headers)
34
+ post(smart_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
35
+ end
36
+
33
37
  run do
34
38
  skip_if refresh_token.blank?
35
39
 
@@ -43,7 +47,7 @@ module SMARTAppLaunch
43
47
 
44
48
  add_credentials_to_request(oauth2_headers, oauth2_params)
45
49
 
46
- post(smart_token_url, body: oauth2_params, name: :token_refresh, headers: oauth2_headers)
50
+ make_auth_token_request(smart_token_url, oauth2_params, oauth2_headers)
47
51
 
48
52
  assert_response_status(200)
49
53
  assert_valid_json(request.response_body)
@@ -52,14 +56,14 @@ module SMARTAppLaunch
52
56
 
53
57
  token_response_body = JSON.parse(request.response_body)
54
58
  output smart_credentials: {
55
- refresh_token: token_response_body['refresh_token'].presence || refresh_token,
56
- access_token: token_response_body['access_token'],
57
- expires_in: token_response_body['expires_in'],
58
- client_id: client_id,
59
- client_secret: client_secret,
60
- token_retrieval_time: token_retrieval_time,
61
- token_url: smart_token_url
62
- }.to_json
59
+ refresh_token: token_response_body['refresh_token'].presence || refresh_token,
60
+ access_token: token_response_body['access_token'],
61
+ expires_in: token_response_body['expires_in'],
62
+ client_id:,
63
+ client_secret:,
64
+ token_retrieval_time:,
65
+ token_url: smart_token_url
66
+ }.to_json
63
67
  end
64
68
  end
65
69
  end
@@ -1,3 +1,3 @@
1
1
  module SMARTAppLaunch
2
- VERSION = '0.4.4'.freeze
2
+ VERSION = '0.4.6'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
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.4
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen MacVicar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-04 00:00:00.000000000 Z
11
+ date: 2024-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inferno_core
@@ -157,7 +157,12 @@ files:
157
157
  - lib/smart_app_launch/backend_services_invalid_jwt_test.rb
158
158
  - lib/smart_app_launch/client_assertion_builder.rb
159
159
  - lib/smart_app_launch/code_received_test.rb
160
+ - lib/smart_app_launch/cors_metadata_request_test.rb
161
+ - lib/smart_app_launch/cors_openid_fhir_user_claim_test.rb
162
+ - lib/smart_app_launch/cors_token_exchange_test.rb
163
+ - lib/smart_app_launch/cors_well_known_endpoint_test.rb
160
164
  - lib/smart_app_launch/discovery_stu1_group.rb
165
+ - lib/smart_app_launch/discovery_stu2_2_group.rb
161
166
  - lib/smart_app_launch/discovery_stu2_group.rb
162
167
  - lib/smart_app_launch/ehr_launch_group.rb
163
168
  - lib/smart_app_launch/ehr_launch_group_stu2.rb
@@ -165,6 +170,7 @@ files:
165
170
  - lib/smart_app_launch/jwks.rb
166
171
  - lib/smart_app_launch/launch_received_test.rb
167
172
  - lib/smart_app_launch/openid_connect_group.rb
173
+ - lib/smart_app_launch/openid_connect_group_stu2_2.rb
168
174
  - lib/smart_app_launch/openid_decode_id_token_test.rb
169
175
  - lib/smart_app_launch/openid_fhir_user_claim_test.rb
170
176
  - lib/smart_app_launch/openid_required_configuration_fields_test.rb
@@ -190,6 +196,7 @@ files:
190
196
  - lib/smart_app_launch/standalone_launch_group.rb
191
197
  - lib/smart_app_launch/standalone_launch_group_stu2.rb
192
198
  - lib/smart_app_launch/standalone_launch_group_stu2_2.rb
199
+ - lib/smart_app_launch/token_exchange_stu2_2_test.rb
193
200
  - lib/smart_app_launch/token_exchange_stu2_test.rb
194
201
  - lib/smart_app_launch/token_exchange_test.rb
195
202
  - lib/smart_app_launch/token_introspection_access_token_group.rb
@@ -234,7 +241,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
241
  - !ruby/object:Gem::Version
235
242
  version: '0'
236
243
  requirements: []
237
- rubygems_version: 3.5.9
244
+ rubygems_version: 3.5.7
238
245
  signing_key:
239
246
  specification_version: 4
240
247
  summary: Inferno Tests for the SMART Application Launch Framework Implementation Guide