module SMARTAppLaunch::TokenPayloadValidation
def check_fhir_context_canonical(canonical)
def check_fhir_context_canonical(canonical) assert canonical.is_a?(String), "`#{canonical.inspect}` is not a String" assert canonical.start_with?('http'), "`#{canonical}` is not a canonical reference" split_canonical = canonical.split('/') if split_canonical.last.start_with?(/&|\|/) resource_type = split_canonical[-3] id = split_canonical[-2] else resource_type = split_canonical[-2] id = split_canonical.last.split(/&|\|/).first end assert FHIR_RESOURCE_TYPES.include?(resource_type), "`#{resource_type}` in `canonical` is not a valid FHIR resource type" assert id.match?(FHIR_ID_REGEX), "`#{id}` in `canonical` is not a valid FHIR id" end
def check_fhir_context_identifier(identifier)
def check_fhir_context_identifier(identifier) assert identifier.is_a?(Hash), "`#{identifier.inspect}` is not an Object" end
def check_fhir_context_reference(reference)
def check_fhir_context_reference(reference) assert reference.is_a?(String), "`#{reference.inspect}` is not a String" assert !reference.start_with?('http'), "`#{reference}` is not a relative reference" resource_type, id = reference.split('/') assert FHIR_RESOURCE_TYPES.include?(resource_type), "`#{resource_type}` in `reference` is not a valid FHIR resource type" assert id.match?(FHIR_ID_REGEX), "`#{id}` in `reference` is not a valid FHIR id" end
def check_for_missing_scopes(requested_scopes, body)
def check_for_missing_scopes(requested_scopes, body) expected_scopes = requested_scopes&.split || [] new_scopes = body['scope']&.split || [] missing_scopes = expected_scopes - new_scopes warning do missing_scopes_string = missing_scopes.map { |scope| "`#{scope}`" }.join(', ') assert missing_scopes.empty?, %( Token exchange response did not include all requested scopes. These may have been denied by user: #{missing_scopes_string}. ) end end
def validate_fhir_context(fhir_context)
def validate_fhir_context(fhir_context) return if fhir_context.nil? assert fhir_context.is_a?(Array), "`fhirContext` field is a #{fhir_context.class.name}, but should be an Array" fhir_context.each do |reference| assert reference.is_a?(String), "`#{reference.inspect}` is not a string" end fhir_context.each do |reference| assert !reference.start_with?('http'), "`#{reference}` is not a relative reference" resource_type, id = reference.split('/') assert FHIR_RESOURCE_TYPES.include?(resource_type), "`#{resource_type}` is not a valid FHIR resource type" assert id.match?(FHIR_ID_REGEX), "`#{id}` is not a valid FHIR id" end end
def validate_fhir_context_stu2_2(fhir_context)
def validate_fhir_context_stu2_2(fhir_context) return if fhir_context.nil? assert fhir_context.is_a?(Array), "`fhirContext` field is a #{fhir_context.class.name}, but should be an Array" fhir_context.each do |reference| assert reference.is_a?(Hash), "`#{reference.inspect}` is not an Object" end fhir_context.each do |context| reference = context['reference'] canonical = context['canonical'] identifier = context['identifier'] type = context['type'] assert reference.present? || canonical.present? || identifier.present?, '`fhirContext` array SHALL include at least one of "reference", "canonical", or "identifier"' check_fhir_context_reference(reference) if reference.present? check_fhir_context_canonical(canonical) if canonical.present? check_fhir_context_identifier(identifier) if identifier.present? if (canonical.present? || identifier.present?) && type.blank? info 'The `type` field is recommended when "canonical" or "identifier" is present in `fhirContext` object' end next unless type.present? assert FHIR_RESOURCE_TYPES.include?(type), "`#{type}` in `type` is not a valid FHIR resource type" end end
def validate_required_fields_present(body, required_fields)
def validate_required_fields_present(body, required_fields) missing_fields = required_fields.select { |field| body[field].blank? } missing_fields_string = missing_fields.map { |field| "`#{field}`" }.join(', ') assert missing_fields.empty?, "Token exchange response did not include all required fields: #{missing_fields_string}." end
def validate_scope_subset(received_scopes, original_scopes)
def validate_scope_subset(received_scopes, original_scopes) extra_scopes = received_scopes.split - original_scopes.split assert extra_scopes.empty?, 'Token response contained scopes which are not a subset of the scope granted to the ' \ "original access token: #{extra_scopes.join(', ')}" end
def validate_token_field_types(body)
def validate_token_field_types(body) STRING_FIELDS .select { |field| body[field].present? } .each do |field| assert body[field].is_a?(String), "Expected `#{field}` to be a String, but found #{body[field].class.name}" end NUMERIC_FIELDS .select { |field| body[field].present? } .each do |field| assert body[field].is_a?(Numeric), "Expected `#{field}` to be a Numeric, but found #{body[field].class.name}" end end
def validate_token_type(body)
def validate_token_type(body) assert body['token_type'].casecmp('bearer').zero?, '`token_type` must be `bearer`' end