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.
- checksums.yaml +4 -4
- data/config/presets/SMART_RunClientAgainstServer.json.erb +31 -0
- data/config/presets/SMART_RunServerAgainstClient.json.erb +42 -0
- data/config/presets/inferno_reference_server_preset.json +15 -86
- data/config/presets/inferno_reference_server_stu2_2_preset.json +20 -69
- data/config/presets/inferno_reference_server_stu2_preset.json +20 -69
- data/lib/smart_app_launch/app_redirect_test.rb +12 -44
- data/lib/smart_app_launch/app_redirect_test_stu2.rb +2 -17
- data/lib/smart_app_launch/backend_services_authorization_group.rb +33 -59
- data/lib/smart_app_launch/backend_services_authorization_request_builder.rb +22 -9
- data/lib/smart_app_launch/backend_services_authorization_request_success_test.rb +32 -24
- data/lib/smart_app_launch/backend_services_authorization_response_body_test.rb +23 -5
- data/lib/smart_app_launch/backend_services_invalid_client_assertion_test.rb +30 -25
- data/lib/smart_app_launch/backend_services_invalid_grant_type_test.rb +30 -24
- data/lib/smart_app_launch/backend_services_invalid_jwt_test.rb +31 -26
- data/lib/smart_app_launch/client_assertion_builder.rb +27 -12
- data/lib/smart_app_launch/client_stu2_2_suite.rb +79 -0
- data/lib/smart_app_launch/client_suite/client_access_group.rb +26 -0
- data/lib/smart_app_launch/client_suite/client_access_interaction_test.rb +64 -0
- data/lib/smart_app_launch/client_suite/client_registration_group.rb +15 -0
- data/lib/smart_app_launch/client_suite/client_registration_verification_test.rb +52 -0
- data/lib/smart_app_launch/client_suite/client_token_request_verification_test.rb +146 -0
- data/lib/smart_app_launch/client_suite/client_token_use_verification_test.rb +47 -0
- data/lib/smart_app_launch/cors_openid_fhir_user_claim_test.rb +2 -2
- data/lib/smart_app_launch/cors_token_exchange_test.rb +2 -2
- data/lib/smart_app_launch/discovery_stu1_group.rb +6 -2
- data/lib/smart_app_launch/docs/demo/FHIR Request.postman_collection.json +81 -0
- data/lib/smart_app_launch/docs/smart_stu2_2_client_suite_description.md +121 -0
- data/lib/smart_app_launch/ehr_launch_group.rb +41 -24
- data/lib/smart_app_launch/ehr_launch_group_stu2.rb +26 -10
- data/lib/smart_app_launch/ehr_launch_group_stu2_2.rb +0 -16
- data/lib/smart_app_launch/endpoints/echoing_fhir_responder.rb +52 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server/token.rb +27 -0
- data/lib/smart_app_launch/endpoints/mock_smart_server.rb +217 -0
- data/lib/smart_app_launch/metadata.rb +2 -2
- data/lib/smart_app_launch/openid_fhir_user_claim_test.rb +5 -4
- data/lib/smart_app_launch/openid_token_payload_test.rb +6 -8
- data/lib/smart_app_launch/smart_stu1_suite.rb +32 -24
- data/lib/smart_app_launch/smart_stu2_2_suite.rb +57 -30
- data/lib/smart_app_launch/smart_stu2_suite.rb +57 -31
- data/lib/smart_app_launch/smart_tls_test.rb +14 -0
- data/lib/smart_app_launch/standalone_launch_group.rb +42 -25
- data/lib/smart_app_launch/standalone_launch_group_stu2.rb +26 -10
- data/lib/smart_app_launch/standalone_launch_group_stu2_2.rb +0 -16
- data/lib/smart_app_launch/tags.rb +7 -0
- data/lib/smart_app_launch/token_exchange_stu2_2_test.rb +5 -17
- data/lib/smart_app_launch/token_exchange_stu2_test.rb +8 -67
- data/lib/smart_app_launch/token_exchange_test.rb +18 -38
- data/lib/smart_app_launch/token_introspection_access_token_group.rb +12 -4
- data/lib/smart_app_launch/token_introspection_access_token_group_stu2_2.rb +9 -1
- data/lib/smart_app_launch/token_introspection_group.rb +2 -4
- data/lib/smart_app_launch/token_introspection_request_group.rb +2 -4
- data/lib/smart_app_launch/token_introspection_response_group.rb +64 -49
- data/lib/smart_app_launch/token_refresh_body_test.rb +9 -2
- data/lib/smart_app_launch/token_refresh_stu2_test.rb +10 -17
- data/lib/smart_app_launch/token_refresh_test.rb +19 -20
- data/lib/smart_app_launch/token_response_body_test.rb +14 -4
- data/lib/smart_app_launch/token_response_body_test_stu2_2.rb +3 -2
- data/lib/smart_app_launch/urls.rb +40 -0
- data/lib/smart_app_launch/version.rb +2 -2
- data/lib/smart_app_launch/well_known_endpoint_test.rb +11 -1
- data/lib/smart_app_launch_test_kit.rb +1 -0
- metadata +21 -4
@@ -40,23 +40,35 @@ module SMARTAppLaunch
|
|
40
40
|
|
41
41
|
config(
|
42
42
|
inputs: {
|
43
|
-
|
44
|
-
name: :
|
45
|
-
title: 'EHR Launch
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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: :
|
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
|
-
|
112
|
-
|
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: :
|
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
|
-
|
128
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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, :
|
12
|
-
input :
|
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
|
-
|
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
|
-
|
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?,
|
61
|
-
assert sub_value.length < 256,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|