smart_app_launch_test_kit 0.5.1 → 0.6.1

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/SMART_RunClientAgainstServer.json.erb +31 -0
  3. data/config/presets/SMART_RunServerAgainstClient.json.erb +42 -0
  4. data/config/presets/inferno_reference_server_preset.json +15 -86
  5. data/config/presets/inferno_reference_server_stu2_2_preset.json +20 -69
  6. data/config/presets/inferno_reference_server_stu2_preset.json +20 -69
  7. data/lib/smart_app_launch/app_redirect_test.rb +12 -44
  8. data/lib/smart_app_launch/app_redirect_test_stu2.rb +2 -17
  9. data/lib/smart_app_launch/backend_services_authorization_group.rb +33 -59
  10. data/lib/smart_app_launch/backend_services_authorization_request_builder.rb +22 -9
  11. data/lib/smart_app_launch/backend_services_authorization_request_success_test.rb +32 -24
  12. data/lib/smart_app_launch/backend_services_authorization_response_body_test.rb +23 -5
  13. data/lib/smart_app_launch/backend_services_invalid_client_assertion_test.rb +30 -25
  14. data/lib/smart_app_launch/backend_services_invalid_grant_type_test.rb +30 -24
  15. data/lib/smart_app_launch/backend_services_invalid_jwt_test.rb +31 -26
  16. data/lib/smart_app_launch/client_assertion_builder.rb +27 -12
  17. data/lib/smart_app_launch/client_stu2_2_suite.rb +79 -0
  18. data/lib/smart_app_launch/client_suite/client_access_group.rb +26 -0
  19. data/lib/smart_app_launch/client_suite/client_access_interaction_test.rb +64 -0
  20. data/lib/smart_app_launch/client_suite/client_registration_group.rb +15 -0
  21. data/lib/smart_app_launch/client_suite/client_registration_verification_test.rb +52 -0
  22. data/lib/smart_app_launch/client_suite/client_token_request_verification_test.rb +146 -0
  23. data/lib/smart_app_launch/client_suite/client_token_use_verification_test.rb +47 -0
  24. data/lib/smart_app_launch/cors_openid_fhir_user_claim_test.rb +2 -2
  25. data/lib/smart_app_launch/cors_token_exchange_test.rb +2 -2
  26. data/lib/smart_app_launch/discovery_stu1_group.rb +6 -2
  27. data/lib/smart_app_launch/docs/demo/FHIR Request.postman_collection.json +81 -0
  28. data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +121 -0
  29. data/lib/smart_app_launch/ehr_launch_group.rb +41 -24
  30. data/lib/smart_app_launch/ehr_launch_group_stu2.rb +26 -10
  31. data/lib/smart_app_launch/ehr_launch_group_stu2_2.rb +0 -16
  32. data/lib/smart_app_launch/endpoints/echoing_fhir_responder.rb +52 -0
  33. data/lib/smart_app_launch/endpoints/mock_smart_server/token.rb +27 -0
  34. data/lib/smart_app_launch/endpoints/mock_smart_server.rb +217 -0
  35. data/lib/smart_app_launch/metadata.rb +2 -2
  36. data/lib/smart_app_launch/openid_fhir_user_claim_test.rb +5 -4
  37. data/lib/smart_app_launch/openid_token_payload_test.rb +6 -8
  38. data/lib/smart_app_launch/smart_stu1_suite.rb +32 -24
  39. data/lib/smart_app_launch/smart_stu2_2_suite.rb +57 -30
  40. data/lib/smart_app_launch/smart_stu2_suite.rb +57 -31
  41. data/lib/smart_app_launch/smart_tls_test.rb +14 -0
  42. data/lib/smart_app_launch/standalone_launch_group.rb +42 -25
  43. data/lib/smart_app_launch/standalone_launch_group_stu2.rb +26 -10
  44. data/lib/smart_app_launch/standalone_launch_group_stu2_2.rb +0 -16
  45. data/lib/smart_app_launch/tags.rb +7 -0
  46. data/lib/smart_app_launch/token_exchange_stu2_2_test.rb +5 -17
  47. data/lib/smart_app_launch/token_exchange_stu2_test.rb +8 -67
  48. data/lib/smart_app_launch/token_exchange_test.rb +18 -38
  49. data/lib/smart_app_launch/token_introspection_access_token_group.rb +12 -4
  50. data/lib/smart_app_launch/token_introspection_access_token_group_stu2_2.rb +9 -1
  51. data/lib/smart_app_launch/token_introspection_group.rb +2 -4
  52. data/lib/smart_app_launch/token_introspection_request_group.rb +2 -4
  53. data/lib/smart_app_launch/token_introspection_response_group.rb +64 -49
  54. data/lib/smart_app_launch/token_refresh_body_test.rb +9 -2
  55. data/lib/smart_app_launch/token_refresh_stu2_test.rb +10 -17
  56. data/lib/smart_app_launch/token_refresh_test.rb +19 -20
  57. data/lib/smart_app_launch/token_response_body_test.rb +14 -4
  58. data/lib/smart_app_launch/token_response_body_test_stu2_2.rb +3 -2
  59. data/lib/smart_app_launch/urls.rb +40 -0
  60. data/lib/smart_app_launch/version.rb +2 -2
  61. data/lib/smart_app_launch/well_known_endpoint_test.rb +11 -1
  62. data/lib/smart_app_launch_test_kit.rb +1 -0
  63. metadata +21 -4
@@ -13,79 +13,20 @@ module SMARTAppLaunch
13
13
  )
14
14
  id :smart_token_exchange_stu2
15
15
 
16
- input :client_auth_encryption_method,
17
- title: 'Encryption Method (Confidential Asymmetric Client Auth Only)',
18
- type: 'radio',
19
- default: 'ES384',
20
- options: {
21
- list_options: [
22
- {
23
- label: 'ES384',
24
- value: 'ES384'
25
- },
26
- {
27
- label: 'RS384',
28
- value: 'RS384'
29
- }
30
- ]
31
- }
32
-
33
- input :client_auth_type,
34
- title: 'Client Authentication Method',
35
- type: 'radio',
36
- default: 'public',
37
- options: {
38
- list_options: [
39
- {
40
- label: 'Public',
41
- value: 'public'
42
- },
43
- {
44
- label: 'Confidential Symmetric',
45
- value: 'confidential_symmetric'
46
- },
47
- {
48
- label: 'Confidential Asymmetric',
49
- value: 'confidential_asymmetric'
50
- }
51
- ]
52
- }
53
-
54
- config(
55
- inputs: {
56
- use_pkce: {
57
- default: 'true',
58
- options: {
59
- list_options: [
60
- {
61
- label: 'Enabled',
62
- value: 'true'
63
- }
64
- ]
65
- }
66
- }
67
- }
68
- )
69
-
70
16
  def add_credentials_to_request(oauth2_params, oauth2_headers)
71
- if client_auth_type == 'confidential_symmetric'
72
- assert client_secret.present?,
73
- "A client secret must be provided when using confidential symmetric client authentication."
74
-
75
- client_credentials = "#{client_id}:#{client_secret}"
76
- oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
77
- elsif client_auth_type == 'public'
78
- oauth2_params[:client_id] = client_id
79
- else
17
+ if smart_auth_info.asymmetric_auth?
80
18
  oauth2_params.merge!(
81
19
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
82
20
  client_assertion: ClientAssertionBuilder.build(
83
- iss: client_id,
84
- sub: client_id,
85
- aud: smart_token_url,
86
- client_auth_encryption_method: client_auth_encryption_method
21
+ iss: smart_auth_info.client_id,
22
+ sub: smart_auth_info.client_id,
23
+ aud: smart_auth_info.token_url,
24
+ client_auth_encryption_method: smart_auth_info.encryption_algorithm,
25
+ custom_jwks: smart_auth_info.jwks
87
26
  )
88
27
  )
28
+ else
29
+ super
89
30
  end
90
31
  end
91
32
  end
@@ -9,30 +9,12 @@ module SMARTAppLaunch
9
9
  RFC6749](https://tools.ietf.org/html/rfc6749#section-4.1.3).
10
10
  )
11
11
  id :smart_token_exchange
12
-
13
- input :code,
14
- :smart_token_url,
15
- :client_id
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
- }
12
+ input :code
33
13
  input :pkce_code_verifier, optional: true
34
- output :token_retrieval_time
35
- output :smart_credentials
14
+ input :smart_auth_info, type: :auth_info, options: { mode: 'auth' }
15
+
16
+ output :token_retrieval_time, :smart_credentials, :smart_auth_info
17
+
36
18
  uses_request :redirect
37
19
  makes_request :token
38
20
 
@@ -45,11 +27,11 @@ module SMARTAppLaunch
45
27
  end
46
28
 
47
29
  def add_credentials_to_request(oauth2_params, oauth2_headers)
48
- if client_secret.present?
49
- client_credentials = "#{client_id}:#{client_secret}"
30
+ if smart_auth_info.symmetric_auth?
31
+ client_credentials = "#{smart_auth_info.client_id}:#{smart_auth_info.client_secret}"
50
32
  oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
51
33
  else
52
- oauth2_params[:client_id] = client_id
34
+ oauth2_params[:client_id] = smart_auth_info.client_id
53
35
  end
54
36
  end
55
37
 
@@ -65,26 +47,24 @@ module SMARTAppLaunch
65
47
 
66
48
  add_credentials_to_request(oauth2_params, oauth2_headers)
67
49
 
68
- oauth2_params[:code_verifier] = pkce_code_verifier if use_pkce == 'true'
50
+ oauth2_params[:code_verifier] = pkce_code_verifier if smart_auth_info.pkce_enabled?
69
51
 
70
- post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
52
+ post(smart_auth_info.token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
71
53
 
72
54
  assert_response_status(200)
73
55
  assert_valid_json(request.response_body)
74
56
 
75
- output token_retrieval_time: Time.now.iso8601
57
+ smart_auth_info.issue_time = Time.now
76
58
 
77
59
  token_response_body = JSON.parse(request.response_body)
78
60
 
79
- output smart_credentials: {
80
- refresh_token: token_response_body['refresh_token'],
81
- access_token: token_response_body['access_token'],
82
- expires_in: token_response_body['expires_in'],
83
- client_id:,
84
- client_secret:,
85
- token_retrieval_time:,
86
- token_url: smart_token_url
87
- }.to_json
61
+ smart_auth_info.refresh_token = token_response_body['refresh_token']
62
+ smart_auth_info.access_token = token_response_body['access_token']
63
+ smart_auth_info.expires_in = token_response_body['expires_in']
64
+
65
+ output smart_credentials: smart_auth_info,
66
+ token_retrieval_time: smart_auth_info.issue_time.iso8601,
67
+ smart_auth_info: smart_auth_info
88
68
  end
89
69
  end
90
70
  end
@@ -15,12 +15,20 @@ module SMARTAppLaunch
15
15
  These tests are currently designed such that the token introspection URL must be present in the SMART well-known endpoint.
16
16
 
17
17
  )
18
-
18
+
19
19
  input_instructions %(
20
20
  Register Inferno as a Standalone SMART App and provide the registration details below.
21
21
  )
22
-
23
- group from: :smart_discovery_stu2
22
+
23
+ group from: :smart_discovery_stu2,
24
+ config: {
25
+ inputs: {
26
+ smart_auth_info: { name: :standalone_smart_auth_info }
27
+ },
28
+ outputs: {
29
+ smart_auth_info: { name: :standalone_smart_auth_info }
30
+ }
31
+ }
24
32
  group from: :smart_standalone_launch_stu2
25
33
  end
26
- end
34
+ end
@@ -20,7 +20,15 @@ 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
+ group from: :smart_discovery_stu2_2,
24
+ config: {
25
+ inputs: {
26
+ smart_auth_info: { name: :standalone_smart_auth_info }
27
+ },
28
+ outputs: {
29
+ smart_auth_info: { name: :standalone_smart_auth_info }
30
+ }
31
+ }
24
32
  group from: :smart_standalone_launch_stu2_2
25
33
  end
26
34
  end
@@ -44,11 +44,9 @@ module SMARTAppLaunch
44
44
  group from: :smart_token_introspection_request_group
45
45
  group from: :smart_token_introspection_response_group
46
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,
47
+ input_order :url, :standalone_smart_auth_info, :custom_authorization_header,
51
48
  :optional_introspection_request_params
49
+
52
50
  input_instructions %(
53
51
  Executing tests at this level will run all three Token Introspection groups back-to-back. If test groups need
54
52
  to be run independently, exit this window and select a specific test group instead.
@@ -66,9 +66,7 @@ module SMARTAppLaunch
66
66
  body are returned in the response.
67
67
  )
68
68
 
69
- input :standalone_access_token,
70
- title: 'Access Token',
71
- description: 'The access token to be introspected. MUST be active.'
69
+ input :standalone_smart_auth_info, type: :auth_info, options: { mode: 'access' }
72
70
 
73
71
  output :active_token_introspection_response_body
74
72
 
@@ -77,7 +75,7 @@ module SMARTAppLaunch
77
75
  skip_if well_known_introspection_url.nil?, 'No introspection URL present in SMART well-known endpoint.'
78
76
 
79
77
  headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' }
80
- body = "token=#{standalone_access_token}"
78
+ body = "token=#{standalone_smart_auth_info.access_token}"
81
79
 
82
80
  if custom_authorization_header.present?
83
81
  custom_headers = custom_authorization_header.split("\n")
@@ -13,20 +13,20 @@ module SMARTAppLaunch
13
13
  in which the access token was given to the client.
14
14
  )
15
15
 
16
- input_instructions %(
17
- There are two categories of input for this test group:
16
+ input_instructions %(
17
+ There are two categories of input for this test group:
18
18
 
19
19
  1. The access token response values, which will dictate what the tests will expect to find in the token
20
20
  introspection response. If the Request New Access Token group was run, these inputs will auto-populate.
21
-
21
+
22
22
  2. The token introspection response bodies. If the Issue Introspection Request test group was run, these will
23
23
  auto-populate; otherwise, the tester will need to an run out-of-band INTROSPECTION requests for a. An ACTIVE
24
24
  access token, AND b. An INACTIVE OR INVALID token
25
25
 
26
26
  See [RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2) for details on active vs inactive tokens.
27
27
  )
28
-
29
- test do
28
+
29
+ test do
30
30
  title 'Token introspection response for an active token contains required fields'
31
31
 
32
32
  description %(
@@ -39,29 +39,26 @@ module SMARTAppLaunch
39
39
  * `scope`, `client_id`, and `exp` claim(s) match between introspection response and access token
40
40
 
41
41
  It is not possible to know what the expected value for `exp` is in advance, so Inferno tests that the claim is
42
- present and represents a time greater than or equal to 10 minutes in the past.
42
+ present and represents a time greater than or equal to 10 minutes in the past.
43
43
 
44
44
  Conditionally Required:
45
- * IF launch context parameter(s) included in access token, introspection response includes claim(s) for
46
- launch context parameter(s)
45
+ * IF launch context parameter(s) included in access token, introspection response includes claim(s) for
46
+ launch context parameter(s)
47
47
  * Parameters checked for are `patient` and `encounter`
48
48
  * IF identity token was included as part of access token response, `iss` and `sub` claims are present in the
49
49
  introspection response and match those of the orignal ID token
50
50
 
51
51
  Optional but Recommended:
52
- * IF identity token was included as part of access token response, `fhirUser` claim SHOULD be present in
52
+ * IF identity token was included as part of access token response, `fhirUser` claim SHOULD be present in
53
53
  introspection response and should match the claim in the ID token
54
54
  )
55
55
 
56
- input :standalone_client_id,
57
- title: 'Access Token client_id',
58
- description: 'ID of the client that requested the access token being introspected'
59
-
56
+ input :standalone_smart_auth_info, type: :auth_info, options: { mode: 'access' }
60
57
 
61
58
  input :standalone_received_scopes,
62
59
  title: 'Expected Introspection Response Value: scope',
63
60
  description: 'A space-separated list of scopes from the original access token response body'
64
-
61
+
65
62
  input :standalone_id_token,
66
63
  title: 'Access Token Response: id_token',
67
64
  type: 'textarea',
@@ -85,16 +82,16 @@ module SMARTAppLaunch
85
82
 
86
83
  def get_json_claim_value(json_response, claim_key)
87
84
  claim_value = json_response[claim_key]
88
- assert claim_value != nil, "Failure: introspection response has no claim for '#{claim_key}'"
89
- return claim_value
85
+ assert !claim_value.nil?, "Failure: introspection response has no claim for '#{claim_key}'"
86
+ claim_value
90
87
  end
91
-
88
+
92
89
  def assert_introspection_response_match(json_response, claim_key, expected_value)
93
90
  expected_value = expected_value.strip
94
91
  claim_value = get_json_claim_value(json_response, claim_key)
95
92
  claim_value = claim_value.strip
96
- assert claim_value.eql?(expected_value),
97
- "Failure: expected introspection response value for '#{claim_key}' to match expected value '#{expected_value}'"
93
+ assert claim_value.eql?(expected_value),
94
+ "Failure: expected introspection response value for '#{claim_key}' to match expected value '#{expected_value}'"
98
95
  end
99
96
 
100
97
  run do
@@ -103,47 +100,57 @@ module SMARTAppLaunch
103
100
  active_introspection_response_body_parsed = JSON.parse(active_token_introspection_response_body)
104
101
 
105
102
  # Required Fields
106
- assert active_introspection_response_body_parsed['active'] == true, "Failure: expected introspection response for 'active' to be Boolean value true for valid token"
107
- assert_introspection_response_match(active_introspection_response_body_parsed, 'client_id', standalone_client_id)
103
+ assert active_introspection_response_body_parsed['active'] == true,
104
+ "Failure: expected introspection response for 'active' to be Boolean value true for valid token"
105
+ assert_introspection_response_match(active_introspection_response_body_parsed, 'client_id',
106
+ standalone_smart_auth_info.client_id)
108
107
 
109
108
  response_scope_value = get_json_claim_value(active_introspection_response_body_parsed, 'scope')
110
109
 
111
110
  # splitting contents and comparing values allows a scope lists with the same contents but different orders to still pass
112
- response_scopes_split = response_scope_value.split()
113
- expected_scopes_split = standalone_received_scopes.split()
111
+ response_scopes_split = response_scope_value.split
112
+ expected_scopes_split = standalone_received_scopes.split
114
113
 
115
- assert response_scopes_split.length() == expected_scopes_split.length(),
116
- "Failure: number of scopes in introspection response, #{response_scopes_split.length()}, does not match number of scopes in access token response, #{expected_scopes_split.length()}"
114
+ assert response_scopes_split.length == expected_scopes_split.length,
115
+ "Failure: number of scopes in introspection response, #{response_scopes_split.length}, does not match number of scopes in access token response, #{expected_scopes_split.length}"
117
116
 
118
117
  expected_scopes_split.each do |scope|
119
- assert response_scopes_split.include?(scope), "Failure: expected scope '#{scope}' not present in introspection response scopes"
118
+ assert response_scopes_split.include?(scope),
119
+ "Failure: expected scope '#{scope}' not present in introspection response scopes"
120
120
  end
121
121
 
122
- # Cannot verify exact value for exp, so instead ensure its value represents a time >= 10 minutes in the past
122
+ # Cannot verify exact value for exp, so instead ensure its value represents a time >= 10 minutes in the past
123
123
  exp = active_introspection_response_body_parsed['exp']
124
- assert exp != nil, "Failure: introspection response has no claim for 'exp'"
125
- current_time = Time.now.to_i
126
- assert exp.to_i >= current_time - 600, "Failure: expired token, exp claim of #{exp} for active token is more than 10 minutes in the past"
127
-
124
+ assert !exp.nil?, "Failure: introspection response has no claim for 'exp'"
125
+ current_time = Time.now.to_i
126
+ assert exp.to_i >= current_time - 600,
127
+ "Failure: expired token, exp claim of #{exp} for active token is more than 10 minutes in the past"
128
+
128
129
  # Conditional fields
129
- assert_introspection_response_match(active_introspection_response_body_parsed, 'patient', standalone_patient_id) if standalone_patient_id.present?
130
- assert_introspection_response_match(active_introspection_response_body_parsed, 'encounter', standalone_encounter_id) if standalone_encounter_id.present?
130
+ if standalone_patient_id.present?
131
+ assert_introspection_response_match(active_introspection_response_body_parsed, 'patient',
132
+ standalone_patient_id)
133
+ end
134
+ if standalone_encounter_id.present?
135
+ assert_introspection_response_match(active_introspection_response_body_parsed, 'encounter',
136
+ standalone_encounter_id)
137
+ end
131
138
 
132
139
  # ID Token Fields
133
140
  if standalone_id_token.present?
134
- id_payload, id_header =
135
- JWT.decode(
136
- standalone_id_token,
137
- nil,
138
- false
139
- )
140
-
141
+ id_payload, =
142
+ JWT.decode(
143
+ standalone_id_token,
144
+ nil,
145
+ false
146
+ )
147
+
141
148
  # Required fields if ID token present
142
149
  id_token_iss = id_payload['iss']
143
150
  id_token_sub = id_payload['sub']
144
151
 
145
- assert id_token_iss != nil, "Failure: ID token from access token response does not have 'iss' claim"
146
- assert id_token_sub != nil, "Failure: ID token from access token response does not have 'sub' claim"
152
+ assert !id_token_iss.nil?, "Failure: ID token from access token response does not have 'iss' claim"
153
+ assert !id_token_sub.nil?, "Failure: ID token from access token response does not have 'sub' claim"
147
154
  assert_introspection_response_match(active_introspection_response_body_parsed, 'iss', id_token_iss)
148
155
  assert_introspection_response_match(active_introspection_response_body_parsed, 'sub', id_token_sub)
149
156
 
@@ -151,9 +158,14 @@ module SMARTAppLaunch
151
158
  fhirUser_id_claim = id_payload['fhirUser']
152
159
  fhirUser_intr_claim = active_introspection_response_body_parsed['fhirUser']
153
160
 
154
- info do
155
- assert fhirUser_intr_claim != nil, "Introspection response SHOULD include claim for fhirUser because ID token present in access token response" if fhirUser_id_claim != nil
156
- assert fhirUser_intr_claim.eql?(fhirUser_id_claim), "Introspection response claim for fhirUser SHOULD match value in ID token" if fhirUser_id_claim != nil
161
+ info do
162
+ unless fhirUser_id_claim.nil?
163
+ assert !fhirUser_intr_claim.nil?,
164
+ 'Introspection response SHOULD include claim for fhirUser because ID token present in access token response'
165
+
166
+ assert fhirUser_intr_claim.eql?(fhirUser_id_claim),
167
+ 'Introspection response claim for fhirUser SHOULD match value in ID token'
168
+ end
157
169
  end
158
170
  end
159
171
  end
@@ -180,12 +192,15 @@ module SMARTAppLaunch
180
192
  description: 'The JSON body of the token introspection response when provided an INVALID token'
181
193
 
182
194
  run do
183
- skip_if invalid_token_introspection_response_body.nil?, 'No invalid introspection response available to validate.'
195
+ skip_if invalid_token_introspection_response_body.nil?,
196
+ 'No invalid introspection response available to validate.'
184
197
  assert_valid_json(invalid_token_introspection_response_body)
185
198
  invalid_token_introspection_response_body_parsed = JSON.parse(invalid_token_introspection_response_body)
186
- assert invalid_token_introspection_response_body_parsed['active'] == false, "Failure: expected introspection response for 'active' to be Boolean value false for invalid token"
187
- assert invalid_token_introspection_response_body_parsed.size == 1, "Failure: expected only 'active' field to be present in introspection response for invalid token"
199
+ assert invalid_token_introspection_response_body_parsed['active'] == false,
200
+ "Failure: expected introspection response for 'active' to be Boolean value false for invalid token"
201
+ assert invalid_token_introspection_response_body_parsed.size == 1,
202
+ "Failure: expected only 'active' field to be present in introspection response for invalid token"
188
203
  end
189
204
  end
190
205
  end
191
- end
206
+ end
@@ -15,7 +15,8 @@ module SMARTAppLaunch
15
15
  Scopes returned must be a strict subset of the scopes granted in the original launch.
16
16
  )
17
17
  input :received_scopes
18
- output :refresh_token, :access_token, :token_retrieval_time, :expires_in, :received_scopes
18
+ input :smart_auth_info, type: :auth_info, options: { mode: 'auth' }
19
+ output :refresh_token, :access_token, :token_retrieval_time, :expires_in, :received_scopes, :smart_auth_info
19
20
  uses_request :token_refresh
20
21
 
21
22
  run do
@@ -25,16 +26,22 @@ module SMARTAppLaunch
25
26
 
26
27
  body = JSON.parse(response[:body])
27
28
  output refresh_token: body['refresh_token'] if body.key? 'refresh_token'
29
+ smart_auth_info.refresh_token = refresh_token if refresh_token.present?
28
30
 
29
31
  required_fields = ['access_token', 'token_type', 'expires_in', 'scope']
30
32
  validate_required_fields_present(body, required_fields)
31
33
 
32
34
  old_received_scopes = received_scopes
35
+ smart_auth_info.issue_time = Time.now
33
36
  output access_token: body['access_token'],
34
- token_retrieval_time: Time.now.iso8601,
37
+ token_retrieval_time: smart_auth_info.issue_time.iso8601,
35
38
  expires_in: body['expires_in'],
36
39
  received_scopes: body['scope']
37
40
 
41
+ smart_auth_info.access_token = access_token
42
+ smart_auth_info.expires_in = expires_in
43
+ output smart_auth_info: smart_auth_info
44
+
38
45
  validate_token_field_types(body)
39
46
  validate_token_type(body)
40
47
 
@@ -16,30 +16,23 @@ module SMARTAppLaunch
16
16
  the Pragma response header field with a value of no-cache to be
17
17
  consistent with the requirements of the inital access token exchange.
18
18
  )
19
- input :client_auth_type
20
- input :client_auth_encryption_method, optional: true
21
- input :client_secret, optional: true
22
19
 
23
- def add_credentials_to_request(oauth2_headers, oauth2_params)
24
- case client_auth_type
25
- when 'public'
26
- oauth2_params['client_id'] = client_id
27
- when 'confidential_symmetric'
28
- assert client_secret.present?,
29
- "A client secret must be provided when using confidential symmetric client authentication."
20
+ input :smart_auth_info, type: :auth_info, options: { mode: 'auth' }
30
21
 
31
- credentials = Base64.strict_encode64("#{client_id}:#{client_secret}")
32
- oauth2_headers['Authorization'] = "Basic #{credentials}"
33
- when 'confidential_asymmetric'
22
+ def add_credentials_to_request(oauth2_headers, oauth2_params)
23
+ if smart_auth_info.asymmetric_auth?
34
24
  oauth2_params.merge!(
35
25
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
36
26
  client_assertion: ClientAssertionBuilder.build(
37
- iss: client_id,
38
- sub: client_id,
39
- aud: smart_token_url,
40
- client_auth_encryption_method: client_auth_encryption_method
27
+ iss: smart_auth_info.client_id,
28
+ sub: smart_auth_info.client_id,
29
+ aud: smart_auth_info.token_url,
30
+ client_auth_encryption_method: smart_auth_info.encryption_algorithm,
31
+ custom_jwks: smart_auth_info.jwks
41
32
  )
42
33
  )
34
+ else
35
+ super
43
36
  end
44
37
  end
45
38
  end
@@ -16,17 +16,18 @@ module SMARTAppLaunch
16
16
  the Pragma response header field with a value of no-cache to be
17
17
  consistent with the requirements of the inital access token exchange.
18
18
  )
19
- input :smart_token_url, :refresh_token, :client_id, :received_scopes
20
- input :client_secret, optional: true
21
- output :smart_credentials, :token_retrieval_time
19
+ input :received_scopes
20
+ input :smart_auth_info, type: :auth_info, options: { mode: 'auth' }
21
+
22
+ output :smart_credentials, :token_retrieval_time, :smart_auth_info
22
23
  makes_request :token_refresh
23
24
 
24
25
  def add_credentials_to_request(oauth2_headers, oauth2_params)
25
- if client_secret.present?
26
- credentials = Base64.strict_encode64("#{client_id}:#{client_secret}")
26
+ if smart_auth_info.symmetric_auth?
27
+ credentials = Base64.strict_encode64("#{smart_auth_info.client_id}:#{smart_auth_info.client_secret}")
27
28
  oauth2_headers['Authorization'] = "Basic #{credentials}"
28
29
  else
29
- oauth2_params['client_id'] = client_id
30
+ oauth2_params['client_id'] = smart_auth_info.client_id
30
31
  end
31
32
  end
32
33
 
@@ -35,11 +36,11 @@ module SMARTAppLaunch
35
36
  end
36
37
 
37
38
  run do
38
- skip_if refresh_token.blank?
39
+ skip_if smart_auth_info.refresh_token.blank?
39
40
 
40
41
  oauth2_params = {
41
42
  'grant_type' => 'refresh_token',
42
- 'refresh_token' => refresh_token
43
+ 'refresh_token' => smart_auth_info.refresh_token
43
44
  }
44
45
  oauth2_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
45
46
 
@@ -47,23 +48,21 @@ module SMARTAppLaunch
47
48
 
48
49
  add_credentials_to_request(oauth2_headers, oauth2_params)
49
50
 
50
- make_auth_token_request(smart_token_url, oauth2_params, oauth2_headers)
51
+ make_auth_token_request(smart_auth_info.token_url, oauth2_params, oauth2_headers)
51
52
 
52
53
  assert_response_status(200)
53
54
  assert_valid_json(request.response_body)
54
55
 
55
- output token_retrieval_time: Time.now.iso8601
56
-
56
+ smart_auth_info.issue_time = Time.now
57
57
  token_response_body = JSON.parse(request.response_body)
58
- output smart_credentials: {
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
58
+
59
+ smart_auth_info.refresh_token = token_response_body['refresh_token'].presence || smart_auth_info.refresh_token
60
+ smart_auth_info.access_token = token_response_body['access_token']
61
+ smart_auth_info.expires_in = token_response_body['expires_in']
62
+
63
+ output smart_credentials: smart_auth_info,
64
+ token_retrieval_time: smart_auth_info.issue_time.iso8601,
65
+ smart_auth_info: smart_auth_info
67
66
  end
68
67
  end
69
68
  end
@@ -16,7 +16,7 @@ module SMARTAppLaunch
16
16
  )
17
17
  id :smart_token_response_body
18
18
 
19
- input :requested_scopes
19
+ input :smart_auth_info, type: :auth_info, options: { mode: 'auth' }
20
20
  output :id_token,
21
21
  :refresh_token,
22
22
  :access_token,
@@ -24,7 +24,9 @@ module SMARTAppLaunch
24
24
  :patient_id,
25
25
  :encounter_id,
26
26
  :received_scopes,
27
- :intent
27
+ :intent,
28
+ :smart_auth_info
29
+
28
30
  uses_request :token
29
31
 
30
32
  run do
@@ -33,6 +35,10 @@ module SMARTAppLaunch
33
35
  assert_valid_json(request.response_body)
34
36
  token_response_body = JSON.parse(request.response_body)
35
37
 
38
+ smart_auth_info.refresh_token = token_response_body['refresh_token']
39
+ smart_auth_info.access_token = token_response_body['access_token']
40
+ smart_auth_info.expires_in = token_response_body['expires_in']
41
+
36
42
  output id_token: token_response_body['id_token'],
37
43
  refresh_token: token_response_body['refresh_token'],
38
44
  access_token: token_response_body['access_token'],
@@ -40,12 +46,16 @@ module SMARTAppLaunch
40
46
  patient_id: token_response_body['patient'],
41
47
  encounter_id: token_response_body['encounter'],
42
48
  received_scopes: token_response_body['scope'],
43
- intent: token_response_body['intent']
49
+ intent: token_response_body['intent'],
50
+ smart_auth_info: smart_auth_info
44
51
 
45
52
  validate_required_fields_present(token_response_body, ['access_token', 'token_type', 'expires_in', 'scope'])
46
53
  validate_token_field_types(token_response_body)
47
54
  validate_token_type(token_response_body)
48
- check_for_missing_scopes(requested_scopes, token_response_body) unless config.options[:ignore_missing_scopes_check]
55
+ unless config.options[:ignore_missing_scopes_check]
56
+ check_for_missing_scopes(smart_auth_info.requested_scopes,
57
+ token_response_body)
58
+ end
49
59
 
50
60
  assert access_token.present?, 'Token response did not contain an access token'
51
61
  assert token_response_body['token_type']&.casecmp('Bearer')&.zero?,