udap_security_test_kit 0.11.3 → 0.11.5
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/UDAP_RunServerAgainstClient.json.erb +4 -4
- data/lib/udap_security_test_kit/client_suite/access_ac_group.rb +25 -0
- data/lib/udap_security_test_kit/client_suite/access_ac_interaction_test.rb +59 -0
- data/lib/udap_security_test_kit/client_suite/access_cc_group.rb +23 -0
- data/lib/udap_security_test_kit/client_suite/access_cc_interaction_test.rb +49 -0
- data/lib/udap_security_test_kit/client_suite/authorization_request_verification_test.rb +83 -0
- data/lib/udap_security_test_kit/client_suite/client_descriptions.rb +70 -0
- data/lib/udap_security_test_kit/client_suite/client_options.rb +20 -0
- data/lib/udap_security_test_kit/client_suite/oidc_jwks.json +32 -0
- data/lib/udap_security_test_kit/client_suite/oidc_jwks.rb +27 -0
- data/lib/udap_security_test_kit/client_suite/registration_ac_group.rb +18 -0
- data/lib/udap_security_test_kit/client_suite/registration_ac_verification_test.rb +38 -0
- data/lib/udap_security_test_kit/client_suite/registration_cc_group.rb +18 -0
- data/lib/udap_security_test_kit/client_suite/registration_cc_verification_test.rb +38 -0
- data/lib/udap_security_test_kit/client_suite/{client_registration_interaction_test.rb → registration_interaction_test.rb} +11 -4
- data/lib/udap_security_test_kit/client_suite/{client_registration_verification_test.rb → registration_request_verification.rb} +38 -40
- data/lib/udap_security_test_kit/client_suite/token_request_ac_verification_test.rb +49 -0
- data/lib/udap_security_test_kit/client_suite/token_request_cc_verification_test.rb +49 -0
- data/lib/udap_security_test_kit/client_suite/{client_token_request_verification_test.rb → token_request_verification.rb} +91 -46
- data/lib/udap_security_test_kit/client_suite/{client_token_use_verification_test.rb → token_use_verification_test.rb} +0 -3
- data/lib/udap_security_test_kit/client_suite.rb +48 -17
- data/lib/udap_security_test_kit/docs/udap_client_suite_description.md +74 -31
- data/lib/udap_security_test_kit/endpoints/echoing_fhir_responder_endpoint.rb +96 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/authorization_endpoint.rb +28 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/introspection_endpoint.rb +34 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/registration_endpoint.rb +31 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/token_endpoint.rb +56 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_authorization_response_creation.rb +63 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_introspection_response_creation.rb +71 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_registration_response_creation.rb +28 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/udap_token_response_creation.rb +218 -0
- data/lib/udap_security_test_kit/endpoints/mock_udap_server.rb +117 -33
- data/lib/udap_security_test_kit/metadata.rb +1 -1
- data/lib/udap_security_test_kit/tags.rb +5 -0
- data/lib/udap_security_test_kit/urls.rb +20 -8
- data/lib/udap_security_test_kit/version.rb +2 -2
- metadata +30 -12
- data/lib/udap_security_test_kit/client_suite/client_access_group.rb +0 -22
- data/lib/udap_security_test_kit/client_suite/client_access_interaction_test.rb +0 -53
- data/lib/udap_security_test_kit/client_suite/client_registration_group.rb +0 -26
- data/lib/udap_security_test_kit/endpoints/echoing_fhir_responder.rb +0 -52
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/registration.rb +0 -57
- data/lib/udap_security_test_kit/endpoints/mock_udap_server/token.rb +0 -27
@@ -0,0 +1,218 @@
|
|
1
|
+
require_relative '../../urls'
|
2
|
+
require_relative '../../tags'
|
3
|
+
require_relative '../mock_udap_server'
|
4
|
+
require_relative '../../client_suite/oidc_jwks'
|
5
|
+
|
6
|
+
module UDAPSecurityTestKit
|
7
|
+
module MockUDAPServer
|
8
|
+
module UDAPTokenResponseCreation
|
9
|
+
def make_udap_authorization_code_token_response # rubocop:disable Metrics/CyclomaticComplexity
|
10
|
+
authorization_code = request.params[:code]
|
11
|
+
client_id = MockUDAPServer.issued_token_to_client_id(authorization_code)
|
12
|
+
software_statement = MockUDAPServer.udap_registration_software_statement(test_run.test_session_id)
|
13
|
+
return unless udap_authenticated?(request.params[:client_assertion], software_statement)
|
14
|
+
|
15
|
+
if MockUDAPServer.token_expired?(authorization_code)
|
16
|
+
MockUDAPServer.update_response_for_expired_token(response, 'Authorization code')
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
return if request.params[:code_verifier].present? && !udap_pkce_valid?(authorization_code)
|
21
|
+
|
22
|
+
exp_min = 60
|
23
|
+
response_body = {
|
24
|
+
access_token: MockUDAPServer.client_id_to_token(client_id, exp_min),
|
25
|
+
token_type: 'Bearer',
|
26
|
+
expires_in: 60 * exp_min
|
27
|
+
}
|
28
|
+
|
29
|
+
launch_context =
|
30
|
+
begin
|
31
|
+
input_string = JSON.parse(result.input_json)&.find do |input|
|
32
|
+
input['name'] == 'launch_context'
|
33
|
+
end&.dig('value')
|
34
|
+
JSON.parse(input_string) if input_string.present?
|
35
|
+
rescue JSON::ParserError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
additional_context = udap_requested_scope_context(udap_registered_scope(software_statement), authorization_code,
|
39
|
+
launch_context)
|
40
|
+
|
41
|
+
response.body = additional_context.merge(response_body).to_json # response body values take priority
|
42
|
+
response.headers['Cache-Control'] = 'no-store'
|
43
|
+
response.headers['Pragma'] = 'no-cache'
|
44
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
45
|
+
response.content_type = 'application/json'
|
46
|
+
response.status = 200
|
47
|
+
end
|
48
|
+
|
49
|
+
def make_udap_refresh_token_response # rubocop:disable Metrics/CyclomaticComplexity
|
50
|
+
refresh_token = request.params[:refresh_token]
|
51
|
+
authorization_code = MockUDAPServer.refresh_token_to_authorization_code(refresh_token)
|
52
|
+
client_id = MockUDAPServer.issued_token_to_client_id(authorization_code)
|
53
|
+
software_statement = MockUDAPServer.udap_registration_software_statement(test_run.test_session_id)
|
54
|
+
return unless udap_authenticated?(request.params[:client_assertion], software_statement)
|
55
|
+
|
56
|
+
# no expiration checks for refresh tokens
|
57
|
+
|
58
|
+
authorization_request = MockUDAPServer.authorization_request_for_code(authorization_code,
|
59
|
+
test_run.test_session_id)
|
60
|
+
if authorization_request.blank?
|
61
|
+
MockUDAPServer.update_response_for_error(
|
62
|
+
response,
|
63
|
+
"no authorization request found for refresh token #{refresh_token}"
|
64
|
+
)
|
65
|
+
return
|
66
|
+
end
|
67
|
+
auth_code_request_inputs = MockUDAPServer.authorization_code_request_details(authorization_request)
|
68
|
+
if auth_code_request_inputs.blank?
|
69
|
+
MockUDAPServer.update_response_for_error(
|
70
|
+
response,
|
71
|
+
'invalid authorization request details'
|
72
|
+
)
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
exp_min = 60
|
77
|
+
response_body = {
|
78
|
+
access_token: MockUDAPServer.client_id_to_token(client_id, exp_min),
|
79
|
+
token_type: 'Bearer',
|
80
|
+
expires_in: 60 * exp_min
|
81
|
+
}
|
82
|
+
|
83
|
+
launch_context =
|
84
|
+
begin
|
85
|
+
input_string = JSON.parse(result.input_json)&.find do |input|
|
86
|
+
input['name'] == 'launch_context'
|
87
|
+
end&.dig('value')
|
88
|
+
JSON.parse(input_string) if input_string.present?
|
89
|
+
rescue JSON::ParserError
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
additional_context = udap_requested_scope_context(udap_registered_scope(software_statement), authorization_code,
|
93
|
+
launch_context)
|
94
|
+
|
95
|
+
response.body = additional_context.merge(response_body).to_json # response body values take priority
|
96
|
+
response.headers['Cache-Control'] = 'no-store'
|
97
|
+
response.headers['Pragma'] = 'no-cache'
|
98
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
99
|
+
response.content_type = 'application/json'
|
100
|
+
response.status = 200
|
101
|
+
end
|
102
|
+
|
103
|
+
def make_udap_client_credential_token_response
|
104
|
+
assertion = request.params[:client_assertion]
|
105
|
+
client_id = MockUDAPServer.client_id_from_client_assertion(assertion)
|
106
|
+
software_statement = MockUDAPServer.udap_registration_software_statement(test_run.test_session_id)
|
107
|
+
return unless udap_authenticated?(request.params[:client_assertion], software_statement)
|
108
|
+
|
109
|
+
exp_min = 60
|
110
|
+
response_body = {
|
111
|
+
access_token: MockUDAPServer.client_id_to_token(client_id, exp_min),
|
112
|
+
token_type: 'Bearer',
|
113
|
+
expires_in: 60 * exp_min
|
114
|
+
}
|
115
|
+
|
116
|
+
response.body = response_body.to_json
|
117
|
+
response.headers['Cache-Control'] = 'no-store'
|
118
|
+
response.headers['Pragma'] = 'no-cache'
|
119
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
120
|
+
response.content_type = 'application/json'
|
121
|
+
response.status = 200
|
122
|
+
end
|
123
|
+
|
124
|
+
def udap_authenticated?(assertion, software_statement)
|
125
|
+
signature_error = MockUDAPServer.udap_token_signature_verification(assertion, software_statement)
|
126
|
+
|
127
|
+
if signature_error.present?
|
128
|
+
MockUDAPServer.update_response_for_error(response, signature_error)
|
129
|
+
return false
|
130
|
+
end
|
131
|
+
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
def udap_requested_scope_context(requested_scopes, authorization_code, launch_context)
|
136
|
+
context = launch_context.present? ? launch_context : {}
|
137
|
+
scopes_list = requested_scopes&.split || []
|
138
|
+
|
139
|
+
if scopes_list.include?('offline_access') || scopes_list.include?('online_access')
|
140
|
+
context[:refresh_token] = MockUDAPServer.authorization_code_to_refresh_token(authorization_code)
|
141
|
+
end
|
142
|
+
|
143
|
+
context[:id_token] = udap_construct_id_token(scopes_list.include?('fhirUser')) if scopes_list.include?('openid')
|
144
|
+
|
145
|
+
context
|
146
|
+
end
|
147
|
+
|
148
|
+
def udap_construct_id_token(include_fhir_user) # rubocop:disable Metrics/CyclomaticComplexity
|
149
|
+
client_id = JSON.parse(result.input_json)&.find do |input|
|
150
|
+
input['name'] == 'client_id'
|
151
|
+
end&.dig('value')
|
152
|
+
fhir_user_relative_reference = JSON.parse(result.input_json)&.find do |input|
|
153
|
+
input['name'] == 'fhir_user_relative_reference'
|
154
|
+
end&.dig('value')
|
155
|
+
|
156
|
+
subject_id = if fhir_user_relative_reference.present?
|
157
|
+
fhir_user_relative_reference.downcase.gsub('/', '-')
|
158
|
+
else
|
159
|
+
SecureRandom.uuid
|
160
|
+
end
|
161
|
+
|
162
|
+
claims = {
|
163
|
+
iss: client_fhir_base_url,
|
164
|
+
sub: subject_id,
|
165
|
+
aud: client_id,
|
166
|
+
exp: 1.year.from_now.to_i,
|
167
|
+
iat: Time.now.to_i
|
168
|
+
}
|
169
|
+
if include_fhir_user && fhir_user_relative_reference.present?
|
170
|
+
claims[:fhirUser] = "#{client_fhir_base_url}/#{fhir_user_relative_reference}"
|
171
|
+
end
|
172
|
+
|
173
|
+
algorithm = 'RS256'
|
174
|
+
private_key = OIDCJWKS.jwks
|
175
|
+
.select { |key| key[:key_ops]&.include?('sign') }
|
176
|
+
.select { |key| key[:alg] == algorithm }
|
177
|
+
.first
|
178
|
+
|
179
|
+
JWT.encode claims, private_key.signing_key, algorithm, { alg: algorithm, kid: private_key.kid, typ: 'JWT' }
|
180
|
+
end
|
181
|
+
|
182
|
+
def udap_pkce_valid?(authorization_code)
|
183
|
+
authorization_request = MockUDAPServer.authorization_request_for_code(authorization_code,
|
184
|
+
test_run.test_session_id)
|
185
|
+
if authorization_request.blank?
|
186
|
+
MockUDAPServer.update_response_for_error(
|
187
|
+
response,
|
188
|
+
"Could not check code_verifier: no authorization request found that returned code #{authorization_code}"
|
189
|
+
)
|
190
|
+
return false
|
191
|
+
end
|
192
|
+
auth_code_request_inputs = MockUDAPServer.authorization_code_request_details(authorization_request)
|
193
|
+
if auth_code_request_inputs.blank?
|
194
|
+
MockUDAPServer.update_response_for_error(
|
195
|
+
response,
|
196
|
+
"Could not check code_verifier: invalid authorization request details for code #{authorization_code}"
|
197
|
+
)
|
198
|
+
return false
|
199
|
+
end
|
200
|
+
|
201
|
+
verifier = request.params[:code_verifier]
|
202
|
+
challenge = auth_code_request_inputs&.dig('code_challenge')
|
203
|
+
method = auth_code_request_inputs&.dig('code_challenge_method')
|
204
|
+
MockUDAPServer.pkce_valid?(verifier, challenge, method, response)
|
205
|
+
end
|
206
|
+
|
207
|
+
def udap_registered_scope(software_statement_jwt)
|
208
|
+
claims, _headers = begin
|
209
|
+
JWT.decode(software_statement_jwt, nil, false)
|
210
|
+
rescue StandardError
|
211
|
+
return nil
|
212
|
+
end
|
213
|
+
|
214
|
+
claims['scope']
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'jwt'
|
2
2
|
require 'faraday'
|
3
3
|
require 'time'
|
4
|
+
require 'rack/utils'
|
4
5
|
require_relative '../urls'
|
5
6
|
require_relative '../tags'
|
6
7
|
require_relative '../udap_jwt_builder'
|
@@ -20,44 +21,34 @@ module UDAPSecurityTestKit
|
|
20
21
|
udap_authorization_extensions_required: [],
|
21
22
|
udap_certifications_supported: [],
|
22
23
|
udap_certifications_required: [],
|
23
|
-
grant_types_supported: ['client_credentials'],
|
24
|
+
grant_types_supported: ['authorization_code', 'client_credentials', 'refresh_token'],
|
24
25
|
scopes_supported: SUPPORTED_SCOPES,
|
26
|
+
registration_endpoint: base_url + REGISTRATION_PATH,
|
27
|
+
registration_endpoint_jwt_signing_alg_values_supported: ['RS256', 'RS384', 'ES384'],
|
28
|
+
authorization_endpoint: base_url + AUTHORIZATION_PATH,
|
25
29
|
token_endpoint: base_url + TOKEN_PATH,
|
26
30
|
token_endpoint_auth_methods_supported: ['private_key_jwt'],
|
27
31
|
token_endpoint_auth_signing_alg_values_supported: ['RS256', 'RS384', 'ES384'],
|
28
|
-
|
29
|
-
registration_endpoint_jwt_signing_alg_values_supported: ['RS256', 'RS384', 'ES384'],
|
32
|
+
introspection_endpoint: base_url + INTROSPECTION_PATH,
|
30
33
|
signed_metadata: udap_signed_metadata_jwt(base_url)
|
31
34
|
}.to_json
|
32
35
|
|
33
36
|
[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
|
34
37
|
end
|
35
38
|
|
36
|
-
def
|
37
|
-
|
38
|
-
client_id = client_id_from_client_assertion(assertion)
|
39
|
-
|
40
|
-
software_statement = udap_registration_software_statement(test_session_id)
|
41
|
-
signature_error = udap_token_signature_verification(assertion, software_statement)
|
42
|
-
|
43
|
-
if signature_error.present?
|
44
|
-
update_response_for_invalid_assertion(response, signature_error)
|
45
|
-
return
|
46
|
-
end
|
47
|
-
|
48
|
-
exp_min = 60
|
39
|
+
def openid_connect_metadata(suite_id)
|
40
|
+
base_url = "#{Inferno::Application['base_url']}/custom/#{suite_id}"
|
49
41
|
response_body = {
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
42
|
+
issuer: base_url + FHIR_PATH,
|
43
|
+
authorization_endpoint: base_url + AUTHORIZATION_PATH,
|
44
|
+
token_endpoint: base_url + TOKEN_PATH,
|
45
|
+
jwks_uri: base_url + OIDC_JWKS_PATH,
|
46
|
+
response_types_supported: ['code', 'id_token', 'token id_token'],
|
47
|
+
subject_types_supported: ['pairwise', 'public'],
|
48
|
+
id_token_signing_alg_values_supported: ['RS256']
|
49
|
+
}.to_json
|
54
50
|
|
55
|
-
|
56
|
-
response.headers['Cache-Control'] = 'no-store'
|
57
|
-
response.headers['Pragma'] = 'no-cache'
|
58
|
-
response.headers['Access-Control-Allow-Origin'] = '*'
|
59
|
-
response.content_type = 'application/json'
|
60
|
-
response.status = 200
|
51
|
+
[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
|
61
52
|
end
|
62
53
|
|
63
54
|
def udap_signed_metadata_jwt(base_url)
|
@@ -68,6 +59,7 @@ module UDAPSecurityTestKit
|
|
68
59
|
iat: Time.now.to_i,
|
69
60
|
jti: SecureRandom.hex(32),
|
70
61
|
token_endpoint: base_url + TOKEN_PATH,
|
62
|
+
authorization_endpoint: base_url + AUTHORIZATION_PATH,
|
71
63
|
registration_endpoint: base_url + REGISTRATION_PATH
|
72
64
|
}.compact
|
73
65
|
|
@@ -132,6 +124,21 @@ module UDAPSecurityTestKit
|
|
132
124
|
JWT.decode(encoded_jwt, nil, false)[0]
|
133
125
|
end
|
134
126
|
|
127
|
+
def udap_client_uri_from_registration_payload(reg_body)
|
128
|
+
udap_claim_from_registration_payload(reg_body, 'iss')
|
129
|
+
end
|
130
|
+
|
131
|
+
def udap_claim_from_registration_payload(reg_body, claim_key)
|
132
|
+
software_statement_jwt = udap_software_statement_jwt(reg_body)
|
133
|
+
return unless software_statement_jwt.present?
|
134
|
+
|
135
|
+
jwt_claims(software_statement_jwt)&.dig(claim_key)
|
136
|
+
end
|
137
|
+
|
138
|
+
def udap_software_statement_jwt(reg_body)
|
139
|
+
reg_body&.dig('software_statement')
|
140
|
+
end
|
141
|
+
|
135
142
|
def client_uri_to_client_id(client_uri)
|
136
143
|
Base64.urlsafe_encode64(client_uri, padding: false)
|
137
144
|
end
|
@@ -151,31 +158,56 @@ module UDAPSecurityTestKit
|
|
151
158
|
end
|
152
159
|
|
153
160
|
def decode_token(token)
|
154
|
-
|
155
|
-
|
161
|
+
token_to_decode =
|
162
|
+
if issued_token_is_refresh_token(token)
|
163
|
+
refresh_token_to_authorization_code(token)
|
164
|
+
else
|
165
|
+
token
|
166
|
+
end
|
167
|
+
return unless token_to_decode.present?
|
168
|
+
|
169
|
+
JSON.parse(Base64.urlsafe_decode64(token_to_decode))
|
170
|
+
rescue StandardError
|
156
171
|
nil
|
157
172
|
end
|
158
173
|
|
159
|
-
def
|
174
|
+
def issued_token_to_client_id(token)
|
160
175
|
decode_token(token)&.dig('client_id')
|
161
176
|
end
|
162
177
|
|
178
|
+
def issued_token_is_refresh_token(token)
|
179
|
+
token.end_with?('_rt')
|
180
|
+
end
|
181
|
+
|
182
|
+
def authorization_code_to_refresh_token(code)
|
183
|
+
"#{code}_rt"
|
184
|
+
end
|
185
|
+
|
186
|
+
def refresh_token_to_authorization_code(refresh_token)
|
187
|
+
refresh_token[..-4]
|
188
|
+
end
|
189
|
+
|
163
190
|
def request_has_expired_token?(request)
|
164
191
|
return false if request.params[:session_path].present?
|
165
192
|
|
166
193
|
token = request.headers['authorization']&.delete_prefix('Bearer ')
|
194
|
+
token_expired?(token)
|
195
|
+
end
|
196
|
+
|
197
|
+
def token_expired?(token, check_time = nil)
|
167
198
|
decoded_token = decode_token(token)
|
168
199
|
return false unless decoded_token&.dig('expiration').present?
|
169
200
|
|
170
|
-
|
201
|
+
check_time = Time.now.to_i unless check_time.present?
|
202
|
+
decoded_token['expiration'] < check_time
|
171
203
|
end
|
172
204
|
|
173
|
-
def update_response_for_expired_token(response)
|
205
|
+
def update_response_for_expired_token(response, type)
|
174
206
|
response.status = 401
|
175
207
|
response.format = :json
|
176
208
|
response.body = FHIR::OperationOutcome.new(
|
177
209
|
issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'expired',
|
178
|
-
details: FHIR::CodeableConcept.new(text:
|
210
|
+
details: FHIR::CodeableConcept.new(text: "#{type} has expired"))
|
179
211
|
).to_json
|
180
212
|
end
|
181
213
|
|
@@ -244,7 +276,7 @@ module UDAPSecurityTestKit
|
|
244
276
|
parsed_body&.dig('software_statement')
|
245
277
|
end
|
246
278
|
|
247
|
-
def
|
279
|
+
def update_response_for_error(response, error_message)
|
248
280
|
response.status = 401
|
249
281
|
response.format = :json
|
250
282
|
response.body = { error: 'invalid_client', error_description: error_message }.to_json
|
@@ -297,5 +329,57 @@ module UDAPSecurityTestKit
|
|
297
329
|
|
298
330
|
nil
|
299
331
|
end
|
332
|
+
|
333
|
+
def pkce_error(verifier, challenge, method)
|
334
|
+
if verifier.blank?
|
335
|
+
'pkce check failed: no verifier provided'
|
336
|
+
elsif challenge.blank?
|
337
|
+
'pkce check failed: no challenge code provided'
|
338
|
+
elsif method == 'S256'
|
339
|
+
return nil unless challenge != calculate_s256_challenge(verifier)
|
340
|
+
|
341
|
+
"invalid S256 pkce verifier: got '#{calculate_s256_challenge(verifier)}' " \
|
342
|
+
"expected '#{challenge}'"
|
343
|
+
else
|
344
|
+
"invalid pkce challenge method '#{method}'"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def pkce_valid?(verifier, challenge, method, response)
|
349
|
+
pkce_error = pkce_error(verifier, challenge, method)
|
350
|
+
|
351
|
+
if pkce_error.present?
|
352
|
+
update_response_for_error(response, pkce_error)
|
353
|
+
false
|
354
|
+
else
|
355
|
+
true
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def calculate_s256_challenge(verifier)
|
360
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
361
|
+
end
|
362
|
+
|
363
|
+
def authorization_request_for_code(code, test_session_id)
|
364
|
+
authorization_requests = Inferno::Repositories::Requests.new.tagged_requests(test_session_id, [AUTHORIZATION_TAG])
|
365
|
+
authorization_requests.find do |request|
|
366
|
+
location_header = request.response_headers.find { |header| header.name.downcase == 'location' }
|
367
|
+
if location_header.present? && location_header.value.present?
|
368
|
+
Rack::Utils.parse_query(URI(location_header.value)&.query)&.dig('code') == code
|
369
|
+
else
|
370
|
+
false
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def authorization_code_request_details(inferno_request)
|
376
|
+
return unless inferno_request.present?
|
377
|
+
|
378
|
+
if inferno_request.verb.downcase == 'get'
|
379
|
+
Rack::Utils.parse_query(URI(inferno_request.url)&.query)
|
380
|
+
elsif inferno_request.verb.downcase == 'post'
|
381
|
+
Rack::Utils.parse_query(inferno_request.request_body)
|
382
|
+
end
|
383
|
+
end
|
300
384
|
end
|
301
385
|
end
|
@@ -9,7 +9,7 @@ module UDAPSecurityTestKit
|
|
9
9
|
STU 1.0 IG](https://hl7.org/fhir/us/udap-security/STU1/index.html)
|
10
10
|
<!-- break -->
|
11
11
|
Specifically, this test
|
12
|
-
kit assesses the required capabilities from the following sections:
|
12
|
+
kit assesses the required capabilities for clients and servers from the following sections:
|
13
13
|
- [JSON Web Token (JWT) Requirements](https://hl7.org/fhir/us/udap-security/STU1/index.html)
|
14
14
|
- [Discovery](https://hl7.org/fhir/us/udap-security/STU1/discovery.html)
|
15
15
|
- [Dynamic Client Registration](https://hl7.org/fhir/us/udap-security/STU1/registration.html)
|
@@ -2,7 +2,12 @@
|
|
2
2
|
|
3
3
|
module UDAPSecurityTestKit
|
4
4
|
REGISTRATION_TAG = 'registration'
|
5
|
+
AUTHORIZATION_TAG = 'authorization'
|
6
|
+
INTROSPECTION_TAG = 'introspection'
|
5
7
|
TOKEN_TAG = 'token'
|
6
8
|
UDAP_TAG = 'udap'
|
7
9
|
ACCESS_TAG = 'access'
|
10
|
+
CLIENT_CREDENTIALS_TAG = 'client_credentials'
|
11
|
+
AUTHORIZATION_CODE_TAG = 'authorization_code'
|
12
|
+
REFRESH_TOKEN_TAG = 'refresh_token'
|
8
13
|
end
|
@@ -2,12 +2,16 @@
|
|
2
2
|
|
3
3
|
module UDAPSecurityTestKit
|
4
4
|
FHIR_PATH = '/fhir'
|
5
|
-
|
6
|
-
|
7
|
-
AUTH_SERVER_PATH = '/auth'
|
5
|
+
OIDC_DISCOVERY_PATH = "#{FHIR_PATH}/.well-known/openid-configuration".freeze
|
6
|
+
OIDC_JWKS_PATH = "#{FHIR_PATH}/.well-known/jwks.json".freeze
|
8
7
|
UDAP_DISCOVERY_PATH = "#{FHIR_PATH}/.well-known/udap".freeze
|
9
|
-
|
8
|
+
AUTH_SERVER_PATH = '/auth'
|
10
9
|
REGISTRATION_PATH = "#{AUTH_SERVER_PATH}/register".freeze
|
10
|
+
AUTHORIZATION_PATH = "#{AUTH_SERVER_PATH}/authorization".freeze
|
11
|
+
INTROSPECTION_PATH = "#{AUTH_SERVER_PATH}/introspect".freeze
|
12
|
+
TOKEN_PATH = "#{AUTH_SERVER_PATH}/token".freeze
|
13
|
+
RESUME_PASS_PATH = '/resume_pass'
|
14
|
+
RESUME_FAIL_PATH = '/resume_fail'
|
11
15
|
|
12
16
|
module URLs
|
13
17
|
def client_base_url
|
@@ -30,14 +34,22 @@ module UDAPSecurityTestKit
|
|
30
34
|
@client_udap_discovery_url ||= client_base_url + UDAP_DISCOVERY_PATH
|
31
35
|
end
|
32
36
|
|
33
|
-
def client_token_url
|
34
|
-
@client_token_url ||= client_base_url + TOKEN_PATH
|
35
|
-
end
|
36
|
-
|
37
37
|
def client_registration_url
|
38
38
|
@client_registration_url ||= client_base_url + REGISTRATION_PATH
|
39
39
|
end
|
40
40
|
|
41
|
+
def client_authorization_url
|
42
|
+
@client_authorization_url ||= client_base_url + AUTHORIZATION_PATH
|
43
|
+
end
|
44
|
+
|
45
|
+
def client_introspection_url
|
46
|
+
@client_introspection_url ||= client_base_url + INTROSPECTION_PATH
|
47
|
+
end
|
48
|
+
|
49
|
+
def client_token_url
|
50
|
+
@client_token_url ||= client_base_url + TOKEN_PATH
|
51
|
+
end
|
52
|
+
|
41
53
|
def client_suite_id
|
42
54
|
UDAPSecurityTestKit::UDAPSecurityClientTestSuite.id
|
43
55
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: udap_security_test_kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.11.
|
4
|
+
version: 0.11.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen MacVicar
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-
|
12
|
+
date: 2025-05-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: inferno_core
|
@@ -65,23 +65,41 @@ files:
|
|
65
65
|
- lib/udap_security_test_kit/client_credentials_group.rb
|
66
66
|
- lib/udap_security_test_kit/client_credentials_token_exchange_test.rb
|
67
67
|
- lib/udap_security_test_kit/client_suite.rb
|
68
|
-
- lib/udap_security_test_kit/client_suite/
|
69
|
-
- lib/udap_security_test_kit/client_suite/
|
70
|
-
- lib/udap_security_test_kit/client_suite/
|
71
|
-
- lib/udap_security_test_kit/client_suite/
|
72
|
-
- lib/udap_security_test_kit/client_suite/
|
73
|
-
- lib/udap_security_test_kit/client_suite/
|
74
|
-
- lib/udap_security_test_kit/client_suite/
|
68
|
+
- lib/udap_security_test_kit/client_suite/access_ac_group.rb
|
69
|
+
- lib/udap_security_test_kit/client_suite/access_ac_interaction_test.rb
|
70
|
+
- lib/udap_security_test_kit/client_suite/access_cc_group.rb
|
71
|
+
- lib/udap_security_test_kit/client_suite/access_cc_interaction_test.rb
|
72
|
+
- lib/udap_security_test_kit/client_suite/authorization_request_verification_test.rb
|
73
|
+
- lib/udap_security_test_kit/client_suite/client_descriptions.rb
|
74
|
+
- lib/udap_security_test_kit/client_suite/client_options.rb
|
75
|
+
- lib/udap_security_test_kit/client_suite/oidc_jwks.json
|
76
|
+
- lib/udap_security_test_kit/client_suite/oidc_jwks.rb
|
77
|
+
- lib/udap_security_test_kit/client_suite/registration_ac_group.rb
|
78
|
+
- lib/udap_security_test_kit/client_suite/registration_ac_verification_test.rb
|
79
|
+
- lib/udap_security_test_kit/client_suite/registration_cc_group.rb
|
80
|
+
- lib/udap_security_test_kit/client_suite/registration_cc_verification_test.rb
|
81
|
+
- lib/udap_security_test_kit/client_suite/registration_interaction_test.rb
|
82
|
+
- lib/udap_security_test_kit/client_suite/registration_request_verification.rb
|
83
|
+
- lib/udap_security_test_kit/client_suite/token_request_ac_verification_test.rb
|
84
|
+
- lib/udap_security_test_kit/client_suite/token_request_cc_verification_test.rb
|
85
|
+
- lib/udap_security_test_kit/client_suite/token_request_verification.rb
|
86
|
+
- lib/udap_security_test_kit/client_suite/token_use_verification_test.rb
|
75
87
|
- lib/udap_security_test_kit/common_assertions.rb
|
76
88
|
- lib/udap_security_test_kit/default_cert_file_loader.rb
|
77
89
|
- lib/udap_security_test_kit/discovery_group.rb
|
78
90
|
- lib/udap_security_test_kit/docs/demo/FHIR Request.postman_collection.json
|
79
91
|
- lib/udap_security_test_kit/docs/udap_client_suite_description.md
|
80
92
|
- lib/udap_security_test_kit/dynamic_client_registration_group.rb
|
81
|
-
- lib/udap_security_test_kit/endpoints/
|
93
|
+
- lib/udap_security_test_kit/endpoints/echoing_fhir_responder_endpoint.rb
|
82
94
|
- lib/udap_security_test_kit/endpoints/mock_udap_server.rb
|
83
|
-
- lib/udap_security_test_kit/endpoints/mock_udap_server/
|
84
|
-
- lib/udap_security_test_kit/endpoints/mock_udap_server/
|
95
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/authorization_endpoint.rb
|
96
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/introspection_endpoint.rb
|
97
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/registration_endpoint.rb
|
98
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/token_endpoint.rb
|
99
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_authorization_response_creation.rb
|
100
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_introspection_response_creation.rb
|
101
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_registration_response_creation.rb
|
102
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_token_response_creation.rb
|
85
103
|
- lib/udap_security_test_kit/grant_types_supported_field_test.rb
|
86
104
|
- lib/udap_security_test_kit/igs/put_ig_package_dot_tgz_here
|
87
105
|
- lib/udap_security_test_kit/metadata.rb
|
@@ -1,22 +0,0 @@
|
|
1
|
-
require_relative 'client_access_interaction_test'
|
2
|
-
require_relative 'client_token_request_verification_test'
|
3
|
-
require_relative 'client_token_use_verification_test'
|
4
|
-
|
5
|
-
module UDAPSecurityTestKit
|
6
|
-
class UDAPClientAccess < Inferno::TestGroup
|
7
|
-
id :udap_client_access
|
8
|
-
title 'Client Access'
|
9
|
-
description %(
|
10
|
-
During these tests, the client system will access Inferno's simulated
|
11
|
-
FHIR server by requesting an access token and making a FHIR request.
|
12
|
-
Inferno will then verify that any token requests made were conformant
|
13
|
-
and that a token returned from a token request was used on an access request.
|
14
|
-
)
|
15
|
-
|
16
|
-
run_as_group
|
17
|
-
|
18
|
-
test from: :udap_client_access_interaction
|
19
|
-
test from: :udap_client_token_request_verification
|
20
|
-
test from: :udap_client_token_use_verification
|
21
|
-
end
|
22
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
require_relative '../urls'
|
2
|
-
require_relative '../endpoints/mock_udap_server'
|
3
|
-
|
4
|
-
module UDAPSecurityTestKit
|
5
|
-
class UDAPClientAccessInteraction < Inferno::Test
|
6
|
-
include URLs
|
7
|
-
|
8
|
-
id :udap_client_access_interaction
|
9
|
-
title 'Perform UDAP-secured Access'
|
10
|
-
description %(
|
11
|
-
During this test, Inferno will wait for the client to access data
|
12
|
-
using a UDAP token obtained during an earlier test.
|
13
|
-
)
|
14
|
-
input :client_id,
|
15
|
-
title: 'Client Id',
|
16
|
-
type: 'text',
|
17
|
-
locked: true,
|
18
|
-
description: %(
|
19
|
-
The registered Client Id for use in obtaining access tokens.
|
20
|
-
Create a new session if you need to change this value.
|
21
|
-
)
|
22
|
-
input :echoed_fhir_response,
|
23
|
-
title: 'FHIR Response to Echo',
|
24
|
-
type: 'textarea',
|
25
|
-
description: %(
|
26
|
-
JSON representation of a FHIR resource for Inferno to echo when a request
|
27
|
-
is made to the simulated FHIR server. The provided content will be echoed
|
28
|
-
back exactly and no check will be made that it is appropriate for the request
|
29
|
-
made. If nothing is provided, an OperationOutcome will be returned.
|
30
|
-
),
|
31
|
-
optional: true
|
32
|
-
|
33
|
-
run do
|
34
|
-
wait(
|
35
|
-
identifier: client_id,
|
36
|
-
message: %(
|
37
|
-
**Access**
|
38
|
-
|
39
|
-
Use the registered client id (#{client_id}) to obtain an access
|
40
|
-
token using the [UDAP B2B client credentials flow](https://hl7.org/fhir/us/udap-security/STU1/b2b.html)
|
41
|
-
and use that token to access a FHIR endpoint under the simulated server's base URL
|
42
|
-
|
43
|
-
`#{client_fhir_base_url}`
|
44
|
-
|
45
|
-
Inferno will echo the response provided in the **FHIR Response to Echo** input.
|
46
|
-
|
47
|
-
[Click here](#{client_resume_pass_url}?token=#{client_id}) once you performed
|
48
|
-
the access.
|
49
|
-
)
|
50
|
-
)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|