udap_security_test_kit 0.11.3 → 0.11.4
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 +46 -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/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_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 +112 -31
- data/lib/udap_security_test_kit/metadata.rb +1 -1
- data/lib/udap_security_test_kit/tags.rb +4 -0
- data/lib/udap_security_test_kit/urls.rb +15 -8
- data/lib/udap_security_test_kit/version.rb +2 -2
- metadata +28 -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,33 @@ 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
|
-
registration_endpoint: base_url + REGISTRATION_PATH,
|
29
|
-
registration_endpoint_jwt_signing_alg_values_supported: ['RS256', 'RS384', 'ES384'],
|
30
32
|
signed_metadata: udap_signed_metadata_jwt(base_url)
|
31
33
|
}.to_json
|
32
34
|
|
33
35
|
[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
|
34
36
|
end
|
35
37
|
|
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
|
38
|
+
def openid_connect_metadata(suite_id)
|
39
|
+
base_url = "#{Inferno::Application['base_url']}/custom/#{suite_id}"
|
49
40
|
response_body = {
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
41
|
+
issuer: base_url + FHIR_PATH,
|
42
|
+
authorization_endpoint: base_url + AUTHORIZATION_PATH,
|
43
|
+
token_endpoint: base_url + TOKEN_PATH,
|
44
|
+
jwks_uri: base_url + OIDC_JWKS_PATH,
|
45
|
+
response_types_supported: ['code', 'id_token', 'token id_token'],
|
46
|
+
subject_types_supported: ['pairwise', 'public'],
|
47
|
+
id_token_signing_alg_values_supported: ['RS256']
|
48
|
+
}.to_json
|
54
49
|
|
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
|
50
|
+
[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
|
61
51
|
end
|
62
52
|
|
63
53
|
def udap_signed_metadata_jwt(base_url)
|
@@ -68,6 +58,7 @@ module UDAPSecurityTestKit
|
|
68
58
|
iat: Time.now.to_i,
|
69
59
|
jti: SecureRandom.hex(32),
|
70
60
|
token_endpoint: base_url + TOKEN_PATH,
|
61
|
+
authorization_endpoint: base_url + AUTHORIZATION_PATH,
|
71
62
|
registration_endpoint: base_url + REGISTRATION_PATH
|
72
63
|
}.compact
|
73
64
|
|
@@ -132,6 +123,21 @@ module UDAPSecurityTestKit
|
|
132
123
|
JWT.decode(encoded_jwt, nil, false)[0]
|
133
124
|
end
|
134
125
|
|
126
|
+
def udap_client_uri_from_registration_payload(reg_body)
|
127
|
+
udap_claim_from_registration_payload(reg_body, 'iss')
|
128
|
+
end
|
129
|
+
|
130
|
+
def udap_claim_from_registration_payload(reg_body, claim_key)
|
131
|
+
software_statement_jwt = udap_software_statement_jwt(reg_body)
|
132
|
+
return unless software_statement_jwt.present?
|
133
|
+
|
134
|
+
jwt_claims(software_statement_jwt)&.dig(claim_key)
|
135
|
+
end
|
136
|
+
|
137
|
+
def udap_software_statement_jwt(reg_body)
|
138
|
+
reg_body&.dig('software_statement')
|
139
|
+
end
|
140
|
+
|
135
141
|
def client_uri_to_client_id(client_uri)
|
136
142
|
Base64.urlsafe_encode64(client_uri, padding: false)
|
137
143
|
end
|
@@ -151,31 +157,56 @@ module UDAPSecurityTestKit
|
|
151
157
|
end
|
152
158
|
|
153
159
|
def decode_token(token)
|
160
|
+
token_to_decode =
|
161
|
+
if issued_token_is_refresh_token(token)
|
162
|
+
refresh_token_to_authorization_code(token)
|
163
|
+
else
|
164
|
+
token
|
165
|
+
end
|
166
|
+
return unless token_to_decode.present?
|
167
|
+
|
154
168
|
JSON.parse(Base64.urlsafe_decode64(token))
|
155
169
|
rescue JSON::ParserError
|
156
170
|
nil
|
157
171
|
end
|
158
172
|
|
159
|
-
def
|
173
|
+
def issued_token_to_client_id(token)
|
160
174
|
decode_token(token)&.dig('client_id')
|
161
175
|
end
|
162
176
|
|
177
|
+
def issued_token_is_refresh_token(token)
|
178
|
+
token.end_with?('_rt')
|
179
|
+
end
|
180
|
+
|
181
|
+
def authorization_code_to_refresh_token(code)
|
182
|
+
"#{code}_rt"
|
183
|
+
end
|
184
|
+
|
185
|
+
def refresh_token_to_authorization_code(refresh_token)
|
186
|
+
refresh_token[..-4]
|
187
|
+
end
|
188
|
+
|
163
189
|
def request_has_expired_token?(request)
|
164
190
|
return false if request.params[:session_path].present?
|
165
191
|
|
166
192
|
token = request.headers['authorization']&.delete_prefix('Bearer ')
|
193
|
+
token_expired?(token)
|
194
|
+
end
|
195
|
+
|
196
|
+
def token_expired?(token, check_time = nil)
|
167
197
|
decoded_token = decode_token(token)
|
168
198
|
return false unless decoded_token&.dig('expiration').present?
|
169
199
|
|
170
|
-
|
200
|
+
check_time = Time.now.to_i unless check_time.present?
|
201
|
+
decoded_token['expiration'] < check_time
|
171
202
|
end
|
172
203
|
|
173
|
-
def update_response_for_expired_token(response)
|
204
|
+
def update_response_for_expired_token(response, type)
|
174
205
|
response.status = 401
|
175
206
|
response.format = :json
|
176
207
|
response.body = FHIR::OperationOutcome.new(
|
177
208
|
issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'expired',
|
178
|
-
details: FHIR::CodeableConcept.new(text:
|
209
|
+
details: FHIR::CodeableConcept.new(text: "#{type}has expired"))
|
179
210
|
).to_json
|
180
211
|
end
|
181
212
|
|
@@ -244,7 +275,7 @@ module UDAPSecurityTestKit
|
|
244
275
|
parsed_body&.dig('software_statement')
|
245
276
|
end
|
246
277
|
|
247
|
-
def
|
278
|
+
def update_response_for_error(response, error_message)
|
248
279
|
response.status = 401
|
249
280
|
response.format = :json
|
250
281
|
response.body = { error: 'invalid_client', error_description: error_message }.to_json
|
@@ -297,5 +328,55 @@ module UDAPSecurityTestKit
|
|
297
328
|
|
298
329
|
nil
|
299
330
|
end
|
331
|
+
|
332
|
+
def pkce_error(verifier, challenge, method)
|
333
|
+
if verifier.blank?
|
334
|
+
'pkce check failed: no verifier provided'
|
335
|
+
elsif challenge.blank?
|
336
|
+
'pkce check failed: no challenge code provided'
|
337
|
+
elsif method == 'S256'
|
338
|
+
return nil unless challenge != calculate_s256_challenge(verifier)
|
339
|
+
|
340
|
+
"invalid S256 pkce verifier: got '#{calculate_s256_challenge(verifier)}' " \
|
341
|
+
"expected '#{challenge}'"
|
342
|
+
else
|
343
|
+
"invalid pkce challenge method '#{method}'"
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def pkce_valid?(verifier, challenge, method, response)
|
348
|
+
pkce_error = pkce_error(verifier, challenge, method)
|
349
|
+
|
350
|
+
if pkce_error.present?
|
351
|
+
update_response_for_error(response, pkce_error)
|
352
|
+
false
|
353
|
+
else
|
354
|
+
true
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def calculate_s256_challenge(verifier)
|
359
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
360
|
+
end
|
361
|
+
|
362
|
+
def authorization_request_for_code(code, test_session_id)
|
363
|
+
authorization_requests = Inferno::Repositories::Requests.new.tagged_requests(test_session_id, [AUTHORIZATION_TAG])
|
364
|
+
authorization_requests.find do |request|
|
365
|
+
location_header = request.response_headers.find { |header| header.name.downcase == 'location' }
|
366
|
+
if location_header.present? && location_header.value.present?
|
367
|
+
Rack::Utils.parse_query(URI(location_header.value)&.query)&.dig('code') == code
|
368
|
+
else
|
369
|
+
false
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def authorization_code_request_details(inferno_request)
|
375
|
+
if inferno_request.verb.downcase == 'get'
|
376
|
+
Rack::Utils.parse_query(URI(inferno_request.url)&.query)
|
377
|
+
elsif inferno_request.verb.downcase == 'post'
|
378
|
+
Rack::Utils.parse_query(inferno_request.request_body)
|
379
|
+
end
|
380
|
+
end
|
300
381
|
end
|
301
382
|
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,11 @@
|
|
2
2
|
|
3
3
|
module UDAPSecurityTestKit
|
4
4
|
REGISTRATION_TAG = 'registration'
|
5
|
+
AUTHORIZATION_TAG = 'authorization'
|
5
6
|
TOKEN_TAG = 'token'
|
6
7
|
UDAP_TAG = 'udap'
|
7
8
|
ACCESS_TAG = 'access'
|
9
|
+
CLIENT_CREDENTIALS_TAG = 'client_credentials'
|
10
|
+
AUTHORIZATION_CODE_TAG = 'authorization_code'
|
11
|
+
REFRESH_TOKEN_TAG = 'refresh_token'
|
8
12
|
end
|
@@ -2,12 +2,15 @@
|
|
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
|
+
TOKEN_PATH = "#{AUTH_SERVER_PATH}/token".freeze
|
12
|
+
RESUME_PASS_PATH = '/resume_pass'
|
13
|
+
RESUME_FAIL_PATH = '/resume_fail'
|
11
14
|
|
12
15
|
module URLs
|
13
16
|
def client_base_url
|
@@ -30,14 +33,18 @@ module UDAPSecurityTestKit
|
|
30
33
|
@client_udap_discovery_url ||= client_base_url + UDAP_DISCOVERY_PATH
|
31
34
|
end
|
32
35
|
|
33
|
-
def client_token_url
|
34
|
-
@client_token_url ||= client_base_url + TOKEN_PATH
|
35
|
-
end
|
36
|
-
|
37
36
|
def client_registration_url
|
38
37
|
@client_registration_url ||= client_base_url + REGISTRATION_PATH
|
39
38
|
end
|
40
39
|
|
40
|
+
def client_authorization_url
|
41
|
+
@client_authorization_url ||= client_base_url + AUTHORIZATION_PATH
|
42
|
+
end
|
43
|
+
|
44
|
+
def client_token_url
|
45
|
+
@client_token_url ||= client_base_url + TOKEN_PATH
|
46
|
+
end
|
47
|
+
|
41
48
|
def client_suite_id
|
42
49
|
UDAPSecurityTestKit::UDAPSecurityClientTestSuite.id
|
43
50
|
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.4
|
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-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: inferno_core
|
@@ -65,23 +65,39 @@ 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/registration_endpoint.rb
|
97
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/token_endpoint.rb
|
98
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_authorization_response_creation.rb
|
99
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_registration_response_creation.rb
|
100
|
+
- lib/udap_security_test_kit/endpoints/mock_udap_server/udap_token_response_creation.rb
|
85
101
|
- lib/udap_security_test_kit/grant_types_supported_field_test.rb
|
86
102
|
- lib/udap_security_test_kit/igs/put_ig_package_dot_tgz_here
|
87
103
|
- 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
|