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
@@ -40,23 +40,35 @@ module SMARTAppLaunch
40
40
 
41
41
  config(
42
42
  inputs: {
43
- client_id: {
44
- name: :ehr_client_id,
45
- title: 'EHR Launch Client ID',
46
- description: 'Client ID provided during registration of Inferno as an EHR launch application'
47
- },
48
- client_secret: {
49
- name: :ehr_client_secret,
50
- title: 'EHR Launch Client Secret',
51
- description: 'Client Secret provided during registration of Inferno as an EHR launch application. ' \
52
- 'Only for clients using confidential symmetric authentication.'
53
- },
54
- requested_scopes: {
55
- name: :ehr_requested_scopes,
56
- title: 'EHR Launch Scope',
57
- description: 'OAuth 2.0 scope provided by system to enable all required functionality',
58
- type: 'textarea',
59
- default: 'launch openid fhirUser offline_access user/*.read'
43
+ smart_auth_info: {
44
+ name: :ehr_smart_auth_info,
45
+ title: 'EHR Launch Credentials',
46
+ options: {
47
+ components: [
48
+ {
49
+ name: :auth_type,
50
+ options: {
51
+ list_options: [
52
+ { label: 'Public', value: 'public' },
53
+ { label: 'Confidential Symmetric', value: 'symmetric' }
54
+ ]
55
+ }
56
+ },
57
+ {
58
+ name: :requested_scopes,
59
+ default: 'launch openid fhirUser offline_access user/*.read'
60
+ },
61
+ {
62
+ name: :use_discovery,
63
+ locked: true
64
+ },
65
+ {
66
+ name: :auth_request_method,
67
+ default: 'GET',
68
+ locked: true
69
+ }
70
+ ]
71
+ }
60
72
  },
61
73
  url: {
62
74
  title: 'EHR Launch FHIR Endpoint',
@@ -88,7 +100,8 @@ module SMARTAppLaunch
88
100
  encounter_id: { name: :ehr_encounter_id },
89
101
  received_scopes: { name: :ehr_received_scopes },
90
102
  intent: { name: :ehr_intent },
91
- smart_credentials: { name: :ehr_smart_credentials }
103
+ smart_credentials: { name: :ehr_smart_credentials },
104
+ smart_auth_info: { name: :ehr_smart_auth_info }
92
105
  },
93
106
  requests: {
94
107
  launch: { name: :ehr_launch },
@@ -99,7 +112,7 @@ module SMARTAppLaunch
99
112
 
100
113
  test from: :smart_app_launch
101
114
  test from: :smart_launch_received
102
- test from: :tls_version_test,
115
+ test from: :smart_tls,
103
116
  id: :ehr_auth_tls,
104
117
  title: 'OAuth 2.0 authorize endpoint secured by transport layer security',
105
118
  description: %(
@@ -108,14 +121,16 @@ module SMARTAppLaunch
108
121
  servers, over TLS-secured channels.
109
122
  ),
110
123
  config: {
111
- inputs: { url: { name: :smart_authorization_url } },
112
- options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
124
+ options: {
125
+ minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION,
126
+ smart_endpoint_key: :auth_url
127
+ }
113
128
  }
114
129
  test from: :smart_app_redirect do
115
130
  input :launch
116
131
  end
117
132
  test from: :smart_code_received
118
- test from: :tls_version_test,
133
+ test from: :smart_tls,
119
134
  id: :ehr_token_tls,
120
135
  title: 'OAuth 2.0 token endpoint secured by transport layer security',
121
136
  description: %(
@@ -124,8 +139,10 @@ module SMARTAppLaunch
124
139
  servers, over TLS-secured channels.
125
140
  ),
126
141
  config: {
127
- inputs: { url: { name: :smart_token_url } },
128
- options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
142
+ options: {
143
+ minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION,
144
+ smart_endpoint_key: :token_url
145
+ }
129
146
  }
130
147
  test from: :smart_token_exchange
131
148
  test from: :smart_token_response_body
@@ -33,16 +33,32 @@ module SMARTAppLaunch
33
33
 
34
34
  config(
35
35
  inputs: {
36
- use_pkce: {
37
- default: 'true',
38
- locked: true
39
- },
40
- pkce_code_challenge_method: {
41
- default: 'S256',
42
- locked: true
43
- },
44
- requested_scopes: {
45
- default: 'launch openid fhirUser offline_access user/*.rs'
36
+ smart_auth_info: {
37
+ name: :ehr_smart_auth_info,
38
+ title: 'EHR Launch Credentials',
39
+ options: {
40
+ components: [
41
+ {
42
+ name: :requested_scopes,
43
+ default: 'launch openid fhirUser offline_access patient/*.rs'
44
+ },
45
+ {
46
+ name: :pkce_support,
47
+ default: 'enabled',
48
+ locked: true
49
+ },
50
+ {
51
+ name: :pkce_code_challenge_method,
52
+ default: 'S256',
53
+ locked: true
54
+ },
55
+ Inferno::DSL::AuthInfo.default_auth_type_component_without_backend_services,
56
+ {
57
+ name: :use_discovery,
58
+ locked: true
59
+ }
60
+ ]
61
+ }
46
62
  }
47
63
  }
48
64
  )
@@ -32,22 +32,6 @@ module SMARTAppLaunch
32
32
  * [SMART EHR Launch Sequence](http://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html#launch-app-ehr-launch)
33
33
  )
34
34
 
35
- config(
36
- inputs: {
37
- use_pkce: {
38
- default: 'true',
39
- locked: true
40
- },
41
- pkce_code_challenge_method: {
42
- default: 'S256',
43
- locked: true
44
- },
45
- requested_scopes: {
46
- default: 'launch openid fhirUser offline_access user/*.rs'
47
- }
48
- }
49
- )
50
-
51
35
  test from: :smart_token_exchange_stu2_2
52
36
 
53
37
  token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'smart_token_exchange' }
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../urls'
4
+ require_relative '../tags'
5
+ require_relative 'mock_smart_server'
6
+
7
+ module SMARTAppLaunch
8
+ class EchoingFHIRResponderEndpoint < Inferno::DSL::SuiteEndpoint
9
+ def test_run_identifier
10
+ MockSMARTServer.token_to_client_id(request.headers['authorization']&.delete_prefix('Bearer '))
11
+ end
12
+
13
+ def make_response
14
+ return if response.status == 401 # set in update_result (expired token handling there)
15
+
16
+ response.content_type = 'application/fhir+json'
17
+
18
+ # If the tester provided a response, echo it
19
+ # otherwise, operation outcome
20
+ echo_response = JSON.parse(result.input_json)
21
+ .find { |input| input['name'].include?('echoed_fhir_response') }
22
+ &.dig('value')
23
+
24
+ unless echo_response.present?
25
+ response.status = 400
26
+ response.body = FHIR::OperationOutcome.new(
27
+ issue: FHIR::OperationOutcome::Issue.new(
28
+ severity: 'fatal', code: 'required',
29
+ details: FHIR::CodeableConcept.new(text: 'No response provided to echo.')
30
+ )
31
+ ).to_json
32
+ return
33
+ end
34
+
35
+ response.status = 200
36
+ response.body = echo_response
37
+ end
38
+
39
+ def update_result
40
+ if MockSMARTServer.request_has_expired_token?(request)
41
+ MockSMARTServer.update_response_for_expired_token(response)
42
+ return
43
+ end
44
+
45
+ nil # never update for now
46
+ end
47
+
48
+ def tags
49
+ [ACCESS_TAG]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../urls'
4
+ require_relative '../../tags'
5
+ require_relative '../mock_smart_server'
6
+
7
+ module SMARTAppLaunch
8
+ module MockSMARTServer
9
+ class TokenEndpoint < Inferno::DSL::SuiteEndpoint
10
+ def test_run_identifier
11
+ MockSMARTServer.client_id_from_client_assertion(request.params[:client_assertion])
12
+ end
13
+
14
+ def make_response
15
+ MockSMARTServer.make_smart_token_response(request, response, result)
16
+ end
17
+
18
+ def update_result
19
+ nil # never update for now
20
+ end
21
+
22
+ def tags
23
+ [TOKEN_TAG, SMART_TAG]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,217 @@
1
+ require 'jwt'
2
+ require 'faraday'
3
+ require 'time'
4
+ require_relative '../urls'
5
+ require_relative '../tags'
6
+
7
+ module SMARTAppLaunch
8
+ module MockSMARTServer
9
+ SUPPORTED_SCOPES = ['openid', 'system/*.read', 'user/*.read', 'patient/*.read'].freeze
10
+
11
+ module_function
12
+
13
+ def smart_server_metadata(suite_id)
14
+ base_url = "#{Inferno::Application['base_url']}/custom/#{suite_id}"
15
+ response_body = {
16
+ token_endpoint_auth_signing_alg_values_supported: ['RS384', 'ES384'],
17
+ capabilities: ['client-confidential-asymmetric'],
18
+ code_challenge_methods_supported: ['S256'],
19
+ token_endpoint_auth_methods_supported: ['private_key_jwt'],
20
+ issuer: base_url + FHIR_PATH,
21
+ grant_types_supported: ['client_credentials'],
22
+ scopes_supported: SUPPORTED_SCOPES,
23
+ token_endpoint: base_url + TOKEN_PATH
24
+ }.to_json
25
+
26
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
27
+ end
28
+
29
+ def make_smart_token_response(request, response, result)
30
+ assertion = request.params[:client_assertion]
31
+ client_id = client_id_from_client_assertion(assertion)
32
+
33
+ key_set_input = JSON.parse(result.input_json)&.find do |input|
34
+ input['name'] == 'smart_jwk_set'
35
+ end&.dig('value')
36
+ signature_error = smart_assertion_signature_verification(assertion, key_set_input)
37
+
38
+ if signature_error.present?
39
+ update_response_for_invalid_assertion(response, signature_error)
40
+ return
41
+ end
42
+
43
+ exp_min = 60
44
+ response_body = {
45
+ access_token: client_id_to_token(client_id, exp_min),
46
+ token_type: 'Bearer',
47
+ expires_in: 60 * exp_min,
48
+ scope: request.params[:scope]
49
+ }
50
+
51
+ response.body = response_body.to_json
52
+ response.headers['Cache-Control'] = 'no-store'
53
+ response.headers['Pragma'] = 'no-cache'
54
+ response.headers['Access-Control-Allow-Origin'] = '*'
55
+ response.content_type = 'application/json'
56
+ response.status = 200
57
+ end
58
+
59
+ def client_id_from_client_assertion(client_assertion_jwt)
60
+ return unless client_assertion_jwt.present?
61
+
62
+ jwt_claims(client_assertion_jwt)&.dig('iss')
63
+ end
64
+
65
+ def parsed_request_body(request)
66
+ JSON.parse(request.request_body)
67
+ rescue JSON::ParserError
68
+ nil
69
+ end
70
+
71
+ def parsed_io_body(request)
72
+ parsed_body = begin
73
+ JSON.parse(request.body.read)
74
+ rescue JSON::ParserError
75
+ nil
76
+ end
77
+ request.body.rewind
78
+
79
+ parsed_body
80
+ end
81
+
82
+ def jwt_claims(encoded_jwt)
83
+ JWT.decode(encoded_jwt, nil, false)[0]
84
+ end
85
+
86
+ def client_uri_to_client_id(client_uri)
87
+ Base64.urlsafe_encode64(client_uri, padding: false)
88
+ end
89
+
90
+ def client_id_to_client_uri(client_id)
91
+ Base64.urlsafe_decode64(client_id)
92
+ end
93
+
94
+ def client_id_to_token(client_id, exp_min)
95
+ token_structure = {
96
+ client_id:,
97
+ expiration: exp_min.minutes.from_now.to_i,
98
+ nonce: SecureRandom.hex(8)
99
+ }.to_json
100
+
101
+ Base64.urlsafe_encode64(token_structure, padding: false)
102
+ end
103
+
104
+ def decode_token(token)
105
+ JSON.parse(Base64.urlsafe_decode64(token))
106
+ rescue JSON::ParserError
107
+ nil
108
+ end
109
+
110
+ def token_to_client_id(token)
111
+ decode_token(token)&.dig('client_id')
112
+ end
113
+
114
+ def jwk_set(jku, warning_messages = []) # rubocop:disable Metrics/CyclomaticComplexity
115
+ jwk_set = JWT::JWK::Set.new
116
+
117
+ if jku.blank?
118
+ warning_messages << 'No key set input.'
119
+ return jwk_set
120
+ end
121
+
122
+ jwk_body = # try as raw jwk set
123
+ begin
124
+ JSON.parse(jku)
125
+ rescue JSON::ParserError
126
+ nil
127
+ end
128
+
129
+ if jwk_body.blank?
130
+ retrieved = Faraday.get(jku) # try as url pointing to a jwk set
131
+ jwk_body =
132
+ begin
133
+ JSON.parse(retrieved.body)
134
+ rescue JSON::ParserError
135
+ warning_messages << "Failed to fetch valid json from jwks uri #{jwk_set}."
136
+ nil
137
+ end
138
+ else
139
+ warning_messages << 'Providing the JWK Set directly is strongly discouraged.'
140
+ end
141
+
142
+ return jwk_set if jwk_body.blank?
143
+
144
+ jwk_body['keys']&.each_with_index do |key_hash, index|
145
+ parsed_key =
146
+ begin
147
+ JWT::JWK.new(key_hash)
148
+ rescue JWT::JWKError => e
149
+ id = key_hash['kid'] | index
150
+ warning_messages << "Key #{id} invalid: #{e}"
151
+ nil
152
+ end
153
+ jwk_set << parsed_key unless parsed_key.blank?
154
+ end
155
+
156
+ jwk_set
157
+ end
158
+
159
+ def request_has_expired_token?(request)
160
+ return false if request.params[:session_path].present?
161
+
162
+ token = request.headers['authorization']&.delete_prefix('Bearer ')
163
+ decoded_token = decode_token(token)
164
+ return false unless decoded_token&.dig('expiration').present?
165
+
166
+ decoded_token['expiration'] < Time.now.to_i
167
+ end
168
+
169
+ def update_response_for_expired_token(response)
170
+ response.status = 401
171
+ response.format = :json
172
+ response.body = FHIR::OperationOutcome.new(
173
+ issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'expired',
174
+ details: FHIR::CodeableConcept.new(text: 'Bearer token has expired'))
175
+ ).to_json
176
+ end
177
+
178
+ def smart_assertion_signature_verification(token, key_set_input) # rubocop:disable Metrics/CyclomaticComplexity
179
+ encoded_token = nil
180
+ if token.is_a?(JWT::EncodedToken)
181
+ encoded_token = token
182
+ else
183
+ begin
184
+ encoded_token = JWT::EncodedToken.new(token)
185
+ rescue StandardError => e
186
+ return "invalid token structure: #{e}"
187
+ end
188
+ end
189
+ return 'invalid token' unless encoded_token.present?
190
+ return 'missing `alg` header' if encoded_token.header['alg'].blank?
191
+ return 'missing `kid` header' if encoded_token.header['kid'].blank?
192
+
193
+ jwk = identify_smart_signing_key(encoded_token.header['kid'], encoded_token.header['jku'], key_set_input)
194
+ return "no key found with `kid` '#{encoded_token.header['kid']}'" if jwk.blank?
195
+
196
+ begin
197
+ encoded_token.verify_signature!(algorithm: encoded_token.header['alg'], key: jwk.verify_key)
198
+ rescue StandardError => e
199
+ return e
200
+ end
201
+
202
+ nil
203
+ end
204
+
205
+ def identify_smart_signing_key(kid, jku, key_set_input)
206
+ key_set = jku.present? ? jku : key_set_input
207
+ parsed_key_set = jwk_set(key_set)
208
+ parsed_key_set&.find { |key| key.kid == kid }
209
+ end
210
+
211
+ def update_response_for_invalid_assertion(response, error_message)
212
+ response.status = 401
213
+ response.format = :json
214
+ response.body = { error: 'invalid_client', error_description: error_message }.to_json
215
+ end
216
+ end
217
+ end
@@ -64,12 +64,12 @@ module SMARTAppLaunch
64
64
  section](https://github.com/inferno-framework/smart-app-launch-test-kit/issues)
65
65
  of the repository.
66
66
  DESCRIPTION
67
- suite_ids [:smart, :smart_stu2, :smart_stu2_2, :smart_access_brands]
67
+ suite_ids [:smart, :smart_stu2, :smart_stu2_2, :smart_access_brands, :smart_client_stu2_2]
68
68
  tags ['SMART App Launch', 'Endpoint Publication']
69
69
  last_updated LAST_UPDATED
70
70
  version VERSION
71
71
  maturity 'Medium'
72
- authors ['Stephen MacVicar']
72
+ authors ['Stephen MacVicar', 'Karl Naden']
73
73
  repo 'https://github.com/inferno-framework/smart-app-launch-test-kit'
74
74
  end
75
75
  end
@@ -8,18 +8,19 @@ module SMARTAppLaunch
8
8
  the url for a Patient, Practitioner, RelatedPerson, or Person resource
9
9
  )
10
10
 
11
- input :id_token_payload_json, :requested_scopes, :url
12
- input :smart_credentials, type: :oauth_credentials
11
+ input :id_token_payload_json, :url
12
+ input :smart_auth_info, type: :auth_info
13
+
13
14
  output :id_token_fhir_user
14
15
 
15
16
  fhir_client do
16
17
  url :url
17
- oauth_credentials :smart_credentials
18
+ auth_info :smart_auth_info
18
19
  end
19
20
 
20
21
  run do
21
22
  skip_if id_token_payload_json.blank?
22
- skip_if !requested_scopes&.include?('fhirUser'), '`fhirUser` scope not requested'
23
+ skip_if !smart_auth_info.requested_scopes&.include?('fhirUser'), '`fhirUser` scope not requested'
23
24
 
24
25
  assert_valid_json(id_token_payload_json)
25
26
  payload = JSON.parse(id_token_payload_json)
@@ -22,16 +22,14 @@ module SMARTAppLaunch
22
22
  REQUIRED_CLAIMS.dup
23
23
  end
24
24
 
25
- input :id_token,
26
- :openid_configuration_json,
27
- :id_token_jwk_json,
28
- :client_id
25
+ input :id_token, :openid_configuration_json, :id_token_jwk_json
26
+ input :smart_auth_info, type: :auth_info, options: { mode: 'auth' }
29
27
 
30
28
  run do
31
29
  skip_if id_token.blank?, 'No ID Token'
32
30
  skip_if openid_configuration_json.blank?, 'No OpenID Configuration found'
33
31
  skip_if id_token_jwk_json.blank?, 'No ID Token jwk found'
34
- skip_if client_id.blank?, 'No Client ID'
32
+ skip_if smart_auth_info.client_id.blank?, 'No Client ID'
35
33
 
36
34
  begin
37
35
  configuration = JSON.parse(openid_configuration_json)
@@ -44,7 +42,7 @@ module SMARTAppLaunch
44
42
  algorithms: ['RS256'],
45
43
  exp_leeway: 60,
46
44
  iss: configuration['issuer'],
47
- aud: client_id,
45
+ aud: smart_auth_info.client_id,
48
46
  verify_not_before: false,
49
47
  verify_iat: false,
50
48
  verify_jti: false,
@@ -57,8 +55,8 @@ module SMARTAppLaunch
57
55
  end
58
56
 
59
57
  sub_value = payload['sub']
60
- assert !sub_value.blank?, "ID token `sub` claim is blank"
61
- assert sub_value.length < 256, "ID token `sub` claim exceeds 255 characters in length"
58
+ assert !sub_value.blank?, 'ID token `sub` claim is blank'
59
+ assert sub_value.length < 256, 'ID token `sub` claim exceeds 255 characters in length'
62
60
 
63
61
  missing_claims = required_claims - payload.keys
64
62
  missing_claims_string = missing_claims.map { |claim| "`#{claim}`" }.join(', ')
@@ -58,16 +58,22 @@ module SMARTAppLaunch
58
58
 
59
59
  run_as_group
60
60
 
61
- group from: :smart_discovery
61
+ group from: :smart_discovery,
62
+ config: {
63
+ inputs: {
64
+ smart_auth_info: { name: :standalone_smart_auth_info }
65
+ },
66
+ outputs: {
67
+ smart_auth_info: { name: :standalone_smart_auth_info }
68
+ }
69
+ }
62
70
  group from: :smart_standalone_launch
63
71
 
64
72
  group from: :smart_openid_connect,
65
73
  config: {
66
74
  inputs: {
67
75
  id_token: { name: :standalone_id_token },
68
- client_id: { name: :standalone_client_id },
69
- requested_scopes: { name: :standalone_requested_scopes },
70
- access_token: { name: :standalone_access_token },
76
+ smart_auth_info: { name: :standalone_smart_auth_info },
71
77
  smart_credentials: { name: :standalone_smart_credentials }
72
78
  }
73
79
  }
@@ -77,9 +83,7 @@ module SMARTAppLaunch
77
83
  title: 'SMART Token Refresh Without Scopes',
78
84
  config: {
79
85
  inputs: {
80
- refresh_token: { name: :standalone_refresh_token },
81
- client_id: { name: :standalone_client_id },
82
- client_secret: { name: :standalone_client_secret },
86
+ smart_auth_info: { name: :standalone_smart_auth_info },
83
87
  received_scopes: { name: :standalone_received_scopes }
84
88
  },
85
89
  outputs: {
@@ -88,7 +92,8 @@ module SMARTAppLaunch
88
92
  access_token: { name: :standalone_access_token },
89
93
  token_retrieval_time: { name: :standalone_token_retrieval_time },
90
94
  expires_in: { name: :standalone_expires_in },
91
- smart_credentials: { name: :standalone_smart_credentials }
95
+ smart_credentials: { name: :standalone_smart_credentials },
96
+ smart_auth_info: { name: :standalone_smart_auth_info }
92
97
  }
93
98
  }
94
99
 
@@ -98,9 +103,7 @@ module SMARTAppLaunch
98
103
  config: {
99
104
  options: { include_scopes: true },
100
105
  inputs: {
101
- refresh_token: { name: :standalone_refresh_token },
102
- client_id: { name: :standalone_client_id },
103
- client_secret: { name: :standalone_client_secret },
106
+ smart_auth_info: { name: :standalone_smart_auth_info },
104
107
  received_scopes: { name: :standalone_received_scopes }
105
108
  },
106
109
  outputs: {
@@ -109,7 +112,8 @@ module SMARTAppLaunch
109
112
  access_token: { name: :standalone_access_token },
110
113
  token_retrieval_time: { name: :standalone_token_retrieval_time },
111
114
  expires_in: { name: :standalone_expires_in },
112
- smart_credentials: { name: :standalone_smart_credentials }
115
+ smart_credentials: { name: :standalone_smart_credentials },
116
+ smart_auth_info: { name: :standalone_smart_auth_info }
113
117
  }
114
118
  }
115
119
  end
@@ -128,7 +132,15 @@ module SMARTAppLaunch
128
132
 
129
133
  run_as_group
130
134
 
131
- group from: :smart_discovery
135
+ group from: :smart_discovery,
136
+ config: {
137
+ inputs: {
138
+ smart_auth_info: { name: :ehr_smart_auth_info }
139
+ },
140
+ outputs: {
141
+ smart_auth_info: { name: :ehr_smart_auth_info }
142
+ }
143
+ }
132
144
 
133
145
  group from: :smart_ehr_launch
134
146
 
@@ -136,9 +148,7 @@ module SMARTAppLaunch
136
148
  config: {
137
149
  inputs: {
138
150
  id_token: { name: :ehr_id_token },
139
- client_id: { name: :ehr_client_id },
140
- requested_scopes: { name: :ehr_requested_scopes },
141
- access_token: { name: :ehr_access_token },
151
+ smart_auth_info: { name: :ehr_smart_auth_info },
142
152
  smart_credentials: { name: :ehr_smart_credentials }
143
153
  }
144
154
  }
@@ -148,9 +158,7 @@ module SMARTAppLaunch
148
158
  title: 'SMART Token Refresh Without Scopes',
149
159
  config: {
150
160
  inputs: {
151
- refresh_token: { name: :ehr_refresh_token },
152
- client_id: { name: :ehr_client_id },
153
- client_secret: { name: :ehr_client_secret },
161
+ smart_auth_info: { name: :ehr_smart_auth_info },
154
162
  received_scopes: { name: :ehr_received_scopes }
155
163
  },
156
164
  outputs: {
@@ -159,7 +167,8 @@ module SMARTAppLaunch
159
167
  access_token: { name: :ehr_access_token },
160
168
  token_retrieval_time: { name: :ehr_token_retrieval_time },
161
169
  expires_in: { name: :ehr_expires_in },
162
- smart_credentials: { name: :ehr_smart_credentials }
170
+ smart_credentials: { name: :ehr_smart_credentials },
171
+ smart_auth_info: { name: :ehr_smart_auth_info }
163
172
  }
164
173
  }
165
174
 
@@ -169,9 +178,7 @@ module SMARTAppLaunch
169
178
  config: {
170
179
  options: { include_scopes: true },
171
180
  inputs: {
172
- refresh_token: { name: :ehr_refresh_token },
173
- client_id: { name: :ehr_client_id },
174
- client_secret: { name: :ehr_client_secret },
181
+ smart_auth_info: { name: :ehr_smart_auth_info },
175
182
  received_scopes: { name: :ehr_received_scopes }
176
183
  },
177
184
  outputs: {
@@ -180,7 +187,8 @@ module SMARTAppLaunch
180
187
  access_token: { name: :ehr_access_token },
181
188
  token_retrieval_time: { name: :ehr_token_retrieval_time },
182
189
  expires_in: { name: :ehr_expires_in },
183
- smart_credentials: { name: :ehr_smart_credentials }
190
+ smart_credentials: { name: :ehr_smart_credentials },
191
+ smart_auth_info: { name: :ehr_smart_auth_info }
184
192
  }
185
193
  }
186
194
  end