From 30e1153248582595795f30fc761a5ba9b78dbaa4 Mon Sep 17 00:00:00 2001 From: Stephen MacVicar Date: Fri, 12 Jul 2024 08:08:40 -0400 Subject: [PATCH] FI-2849: Add token introspection tests (#531) * add token introspection group * update short id map * remove introspection attestation * update procedure metadata * remove us core 6 from test matrix test list * update matrix generation to support three levels of nesting * fix linting error --- lib/onc_certification_g10_test_kit.rb | 3 + .../onc_program_procedure.yml | 13 +- .../short_id_map.yml | 49 +++++++- .../tasks/generate_matrix.rb | 28 +++++ .../token_introspection_group.rb | 115 ++++++++++++++++++ ...isual_inspection_and_attestations_group.rb | 37 ------ 6 files changed, 200 insertions(+), 45 deletions(-) create mode 100644 lib/onc_certification_g10_test_kit/token_introspection_group.rb diff --git a/lib/onc_certification_g10_test_kit.rb b/lib/onc_certification_g10_test_kit.rb index 8ce63e77..e0237c9a 100644 --- a/lib/onc_certification_g10_test_kit.rb +++ b/lib/onc_certification_g10_test_kit.rb @@ -25,6 +25,7 @@ require_relative 'onc_certification_g10_test_kit/multi_patient_api_stu1' require_relative 'onc_certification_g10_test_kit/multi_patient_api_stu2' require_relative 'onc_certification_g10_test_kit/terminology_binding_validator' +require_relative 'onc_certification_g10_test_kit/token_introspection_group' require_relative 'onc_certification_g10_test_kit/token_revocation_group' require_relative 'onc_certification_g10_test_kit/visual_inspection_and_attestations_group' require_relative 'inferno/terminology' @@ -363,6 +364,8 @@ def self.well_known_route_handler group from: :g10_ehr_patient_launch_stu2, required_suite_options: G10Options::SMART_2_REQUIREMENT + group from: :g10_token_introspection + group from: :g10_visual_inspection_and_attestations end end diff --git a/lib/onc_certification_g10_test_kit/onc_program_procedure.yml b/lib/onc_certification_g10_test_kit/onc_program_procedure.yml index 230998e5..026d950e 100644 --- a/lib/onc_certification_g10_test_kit/onc_program_procedure.yml +++ b/lib/onc_certification_g10_test_kit/onc_program_procedure.yml @@ -1198,17 +1198,16 @@ procedure: id: TOK-INTRO-1 SUT: | The health IT developer demonstrates the ability of the Health IT - Module to receive and validate a token it has issued. + Module to receive and validate a token it has issued in accordance + with an implementation specification in § 170.215(c). TLV: | The tester verifies the ability of the Health IT Module to receive and - validate a token it has issued. + validate a token it has issued in accordance with an implementation + specification in § 170.215(c). inferno_supported: 'yes' - inferno_notes: | - No standard is required and therefore Inferno cannot do this in - an automated fashion and this is recorded as an attestation - within Inferno. inferno_tests: - - 9.10.06 + - 9.11.2.01 - 9.11.2.02 + - 9.11.3.01 - 9.11.3.02 - section: Paragraph (g)(10)(ii) – Supported search operations steps: - group: Supported Search Operations for a Single Patient’s Data diff --git a/lib/onc_certification_g10_test_kit/short_id_map.yml b/lib/onc_certification_g10_test_kit/short_id_map.yml index 81247984..ae485418 100644 --- a/lib/onc_certification_g10_test_kit/short_id_map.yml +++ b/lib/onc_certification_g10_test_kit/short_id_map.yml @@ -2103,13 +2103,60 @@ g10_certification-Group06-g10_ehr_patient_launch_stu2-smart_token_response_body: g10_certification-Group06-g10_ehr_patient_launch_stu2-smart_token_response_headers: 9.9.09 g10_certification-Group06-g10_ehr_patient_launch_stu2-g10_patient_context: 9.9.10 g10_certification-Group06-g10_ehr_patient_launch_stu2-g10_patient_scope: 9.9.11 +g10_certification-Group06-g10_token_introspection: '9.11' +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group: 9.11.1 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery_stu2: 9.11.1.1 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery_stu2-well_known_endpoint +: 9.11.1.1.01 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery_stu2-well_known_capabilities_stu2 +: 9.11.1.1.02 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2: 9.11.1.2 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-standalone_auth_tls +: 9.11.1.2.01 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-smart_app_redirect_stu2 +: 9.11.1.2.02 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-smart_code_received +: 9.11.1.2.03 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-standalone_token_tls +: 9.11.1.2.04 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-smart_token_exchange +: 9.11.1.2.05 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-smart_token_response_body +: 9.11.1.2.06 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch_stu2-smart_token_response_headers +: 9.11.1.2.07 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery: 9.11.1.3 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery-Test01: 9.11.1.3.01 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery-Test02: 9.11.1.3.02 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery-Test03: 9.11.1.3.03 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_discovery-Test04: 9.11.1.3.04 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch: 9.11.1.4 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-standalone_auth_tls +: 9.11.1.4.01 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-smart_app_redirect +: 9.11.1.4.02 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-smart_code_received +: 9.11.1.4.03 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-standalone_token_tls +: 9.11.1.4.04 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-smart_token_exchange +: 9.11.1.4.05 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-smart_token_response_body +: 9.11.1.4.06 +? g10_certification-Group06-g10_token_introspection-smart_token_introspection_access_token_group-smart_standalone_launch-smart_token_response_headers +: 9.11.1.4.07 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_request_group: 9.11.2 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_request_group-Test01: 9.11.2.01 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_request_group-Test02: 9.11.2.02 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_response_group: 9.11.3 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_response_group-Test01: 9.11.3.01 +g10_certification-Group06-g10_token_introspection-smart_token_introspection_response_group-Test02: 9.11.3.02 g10_certification-Group06-g10_visual_inspection_and_attestations: '9.10' g10_certification-Group06-g10_visual_inspection_and_attestations-Test01: 9.10.01 g10_certification-Group06-g10_visual_inspection_and_attestations-Test02: 9.10.02 g10_certification-Group06-g10_visual_inspection_and_attestations-Test03: 9.10.03 g10_certification-Group06-g10_visual_inspection_and_attestations-Test04: 9.10.04 g10_certification-Group06-g10_visual_inspection_and_attestations-Test05: 9.10.05 -g10_certification-Group06-g10_visual_inspection_and_attestations-Test06: 9.10.06 g10_certification-Group06-g10_visual_inspection_and_attestations-Test07: 9.10.07 g10_certification-Group06-g10_visual_inspection_and_attestations-Test08: 9.10.08 g10_certification-Group06-g10_visual_inspection_and_attestations-Test09: 9.10.09 diff --git a/lib/onc_certification_g10_test_kit/tasks/generate_matrix.rb b/lib/onc_certification_g10_test_kit/tasks/generate_matrix.rb index 8b74b124..ac1b29ee 100644 --- a/lib/onc_certification_g10_test_kit/tasks/generate_matrix.rb +++ b/lib/onc_certification_g10_test_kit/tasks/generate_matrix.rb @@ -85,6 +85,12 @@ def generate_matrix_worksheet # rubocop:disable Metrics/CyclomaticComplexity # full_test_id = "#{test_case.prefix}#{test.id}" column_map[test.short_id] = col end + + test_case.groups.each do |nested_group| + nested_group.tests.each do |test| + column_map[test.short_id] = col + end + end col += 1 end end @@ -231,6 +237,8 @@ def generate_inferno_test_worksheet # rubocop:disable Metrics/CyclomaticComplexi row = 1 test_suite.groups.each do |group| + next if group.short_id == '6' # Skip US Core 5 + row += 1 inferno_worksheet.add_cell(row, 0, "#{group.short_id}: #{group.title}") inferno_worksheet.add_cell(row, 6, applicable_options(group).map(&:value).uniq.join(', ')) @@ -252,6 +260,26 @@ def generate_inferno_test_worksheet # rubocop:disable Metrics/CyclomaticComplexi inferno_worksheet.change_row_vertical_alignment(row, 'top') row += 1 end + + test_case.groups.each do |nested_group| + inferno_worksheet.add_cell(row, 1, "#{nested_group.short_id}: #{nested_group.title}") + inferno_worksheet.add_cell(row, 6, applicable_options(nested_group).map(&:value).uniq.join(', ')) + + row += 1 + nested_group.tests.each do |test| + this_row = columns.map do |column| + column[2].call(test) + end + + this_row.each_with_index do |value, index| + inferno_worksheet.add_cell(row, index, value).change_text_wrap(true) + end + inferno_worksheet + .change_row_height(row, [26, ((test.description || '').strip.lines.count * 10) + 10].max) + inferno_worksheet.change_row_vertical_alignment(row, 'top') + row += 1 + end + end end end diff --git a/lib/onc_certification_g10_test_kit/token_introspection_group.rb b/lib/onc_certification_g10_test_kit/token_introspection_group.rb new file mode 100644 index 00000000..cf0c765e --- /dev/null +++ b/lib/onc_certification_g10_test_kit/token_introspection_group.rb @@ -0,0 +1,115 @@ +require 'smart_app_launch/standalone_launch_group' +require 'smart_app_launch/discovery_stu1_group' +require 'smart_app_launch/token_introspection_group' + +require_relative 'g10_options' + +module ONCCertificationG10TestKit + class TokenIntrospectionGroup < SMARTAppLaunch::SMARTTokenIntrospectionGroup + id :g10_token_introspection + + description <<~DESCRIPTION + # Background + + OAuth 2.0 Token introspection, as described in + [RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662), allows an + authorized resource server to query an OAuth 2.0 authorization server for + metadata on a token. The [SMART App Launch STU2 Implementation Guide + Section on Token + Introspection](https://hl7.org/fhir/smart-app-launch/STU2/token-introspection.html) + states that + > SMART on FHIR EHRs SHOULD support token introspection, which allows a + > broader ecosystem of resource servers to leverage authorization + > decisions managed by a single authorization server. + + # Test Methodology + + In these tests, Inferno acts as an authorized resource server that queries + the authorization server about an access token, rather than a client to a + FHIR resource server as in the previous SMART App Launch tests. Ideally, + Inferno should be registered with the authorization server as an + authorized resource server capable of accessing the token introspection + endpoint through client credentials, per the SMART IG recommendations. + However, the SMART IG only formally REQUIRES "some form of authorization" + to access the token introspection endpoint and does not specifiy any one + specific approach. As such, the token introspection tests are broken up + into three groups that each complete a discrete step in the token + introspection process: + + 1. **Request Access Token Group** - repeats a subset of Standalone Launch + tests in order to receive a new access token with an authorization code + grant. + 2. **Issue Token Introspection Request Group** - completes the + introspection requests. + 3. **Validate Token Introspection Response Group** - validates the + contents of the introspection responses. + + See the individual test groups for more details and guidance. + DESCRIPTION + + input_instructions <<~INSTRUCTIONS + If the introspection endpoint is protected, testers must enter their own + HTTP Authorization header for the introspection request. See [RFC 7616 The + 'Basic' HTTP Authentication + Scheme](https://datatracker.ietf.org/doc/html/rfc7617) for the most common + approach that uses client credentials. Testers may also provide any + additional parameters needed for their authorization server to complete + the introspection request. + + **Note:** For both the Authorization header and request parameters, user-input + values will be sent exactly as entered and therefore the tester must + URI-encode any appropriate values. + INSTRUCTIONS + + run_as_group + + input :well_known_introspection_url, + title: 'Token Introspection Endpoint', + description: <<~DESCRIPTION, + The complete URL of the token introspection endpoint. This will be + populated automatically if included in the server's discovery + endpoint. + DESCRIPTION + optional: true + + input_order :url, + :well_known_introspection_url, + :custom_authorization_header, + :optional_introspection_request_params, + :standalone_client_id, + :standalone_client_secret, + :authorization_method, + :use_pkce, + :pkce_code_challenge_method, + :standalone_requested_scopes, + :client_auth_encryption_method, + :client_auth_type + + groups.first.description <<~DESCRIPTION + These tests are perform discovery and a standalone launch in order to + receive a new, active access token that will be provided for token + introspection. + DESCRIPTION + + groups[1].description <<~DESCRIPTION + This group of tests executes the token introspection requests and ensures + the correct HTTP response is returned but does not validate the contents + of the token introspection response. + DESCRIPTION + + # The token introspection tests are SMART v2 only, so they use v2 discovery + # and launch groups. g10 needs them for SMART v1 and v2, so this sets the + # original discovery and launch groups to only appear when using SMART v2, + # and adds the v1 groups when using v1. + + groups.first.groups.each do |group| + group.required_suite_options(G10Options::SMART_2_REQUIREMENT) + end + + groups.first.group from: :smart_discovery, + required_suite_options: G10Options::SMART_1_REQUIREMENT + + groups.first.group from: :smart_standalone_launch, + required_suite_options: G10Options::SMART_1_REQUIREMENT + end +end diff --git a/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb b/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb index 6edacaad..bee30d49 100644 --- a/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +++ b/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb @@ -189,43 +189,6 @@ class VisualInspectionAndAttestationsGroup < Inferno::TestGroup end end - test do - title 'Health IT developer demonstrated the ability of the Health IT Module / ' \ - 'authorization server to validate token it has issued.' - description %( - Health IT developer demonstrated the ability of the Health IT Module / - authorization server to validate token it has issued - ) - id 'Test06' - input :token_validation_support, - title: 'Health IT developer demonstrated the ability of the Health IT Module / authorization server to validate token it has issued.', # rubocop:disable Layout/LineLength - type: 'radio', - default: 'false', - options: { - list_options: [ - { - label: 'Yes', - value: 'true' - }, - { - label: 'No', - value: 'false' - } - ] - } - input :token_validation_notes, - title: 'Notes, if applicable:', - type: 'textarea', - optional: true - - run do - assert token_validation_support == 'true', - 'Health IT Module did not demonstrate the ability of the Health IT Module / ' \ - 'authorization server to validate token it has issued' - pass token_validation_notes if token_validation_notes.present? - end - end - test do title 'Tester verifies that all information is accurate and without omission.' description %(