Module: SMARTAppLaunch::TokenPayloadValidation

Included in:
OpenIDTokenPayloadTest, TokenRefreshBodyTest, TokenRefreshTest, TokenResponseBodyTest
Defined in:
lib/smart_app_launch/token_payload_validation.rb

Constant Summary collapse

STRING_FIELDS =
['access_token', 'token_type', 'scope', 'refresh_token'].freeze
NUMERIC_FIELDS =
['expires_in'].freeze
FHIR_RESOURCE_TYPES =

All resource types from DSTU3, STU3, R4, R4B, and R5

[
  "Account", "ActivityDefinition", "ActorDefinition",
  "AdministrableProductDefinition", "AdverseEvent", "AllergyIntolerance",
  "Appointment", "AppointmentResponse", "ArtifactAssessment", "AuditEvent",
  "Basic", "Binary", "BiologicallyDerivedProduct",
  "BiologicallyDerivedProductDispense", "BodySite", "BodyStructure",
  "Bundle", "CapabilityStatement", "CarePlan", "CareTeam", "CatalogEntry",
  "ChargeItem", "ChargeItemDefinition", "Citation", "Claim",
  "ClaimResponse", "ClinicalImpression", "ClinicalUseDefinition",
  "CodeSystem", "Communication", "CommunicationRequest",
  "CompartmentDefinition", "Composition", "ConceptMap", "Condition",
  "ConditionDefinition", "Conformance", "Consent", "Contract", "Coverage",
  "CoverageEligibilityRequest", "CoverageEligibilityResponse",
  "DataElement", "DetectedIssue", "Device", "DeviceAssociation",
  "DeviceComponent", "DeviceDefinition", "DeviceDispense", "DeviceMetric",
  "DeviceRequest", "DeviceUsage", "DeviceUseRequest", "DeviceUseStatement",
  "DiagnosticOrder", "DiagnosticReport", "DocumentManifest",
  "DocumentReference", "EffectEvidenceSynthesis", "EligibilityRequest",
  "EligibilityResponse", "Encounter", "EncounterHistory", "Endpoint",
  "EnrollmentRequest", "EnrollmentResponse", "EpisodeOfCare",
  "EventDefinition", "Evidence", "EvidenceReport", "EvidenceVariable",
  "ExampleScenario", "ExpansionProfile", "ExplanationOfBenefit",
  "FamilyMemberHistory", "Flag", "FormularyItem", "GenomicStudy", "Goal",
  "GraphDefinition", "Group", "GuidanceResponse", "HealthcareService",
  "ImagingManifest", "ImagingObjectSelection", "ImagingSelection",
  "ImagingStudy", "Immunization", "ImmunizationEvaluation",
  "ImmunizationRecommendation", "ImplementationGuide", "Ingredient",
  "InsurancePlan", "InventoryItem", "InventoryReport", "Invoice", "Library",
  "Linkage", "List", "Location", "ManufacturedItemDefinition", "Measure",
  "MeasureReport", "Media", "Medication", "MedicationAdministration",
  "MedicationDispense", "MedicationKnowledge", "MedicationOrder",
  "MedicationRequest", "MedicationStatement", "MedicinalProduct",
  "MedicinalProductAuthorization", "MedicinalProductContraindication",
  "MedicinalProductDefinition", "MedicinalProductIndication",
  "MedicinalProductIngredient", "MedicinalProductInteraction",
  "MedicinalProductManufactured", "MedicinalProductPackaged",
  "MedicinalProductPharmaceutical", "MedicinalProductUndesirableEffect",
  "MessageDefinition", "MessageHeader", "MolecularSequence", "NamingSystem",
  "NutritionIntake", "NutritionOrder", "NutritionProduct", "Observation",
  "ObservationDefinition", "OperationDefinition", "OperationOutcome",
  "Order", "OrderResponse", "Organization", "OrganizationAffiliation",
  "PackagedProductDefinition", "Patient", "PaymentNotice",
  "PaymentReconciliation", "Permission", "Person", "PlanDefinition",
  "Practitioner", "PractitionerRole", "Procedure", "ProcedureRequest",
  "ProcessRequest", "ProcessResponse", "Provenance", "Questionnaire",
  "QuestionnaireResponse", "ReferralRequest", "RegulatedAuthorization",
  "RelatedPerson", "RequestGroup", "RequestOrchestration", "Requirements",
  "ResearchDefinition", "ResearchElementDefinition", "ResearchStudy",
  "ResearchSubject", "RiskAssessment", "RiskEvidenceSynthesis", "Schedule",
  "SearchParameter", "Sequence", "ServiceDefinition", "ServiceRequest",
  "Slot", "Specimen", "SpecimenDefinition", "StructureDefinition",
  "StructureMap", "Subscription", "SubscriptionStatus", "SubscriptionTopic",
  "Substance", "SubstanceDefinition", "SubstanceNucleicAcid",
  "SubstancePolymer", "SubstanceProtein", "SubstanceReferenceInformation",
  "SubstanceSourceMaterial", "SubstanceSpecification", "SupplyDelivery",
  "SupplyRequest", "Task", "TerminologyCapabilities", "TestPlan",
  "TestReport", "TestScript", "Transport", "ValueSet", "VerificationResult",
  "VisionPrescription"
].to_set.freeze
FHIR_ID_REGEX =
/[A-Za-z0-9\-\.]{1,64}(\/_history\/[A-Za-z0-9\-\.]{1,64})?(#[A-Za-z0-9\-\.]{1,64})?/.freeze

Instance Method Summary collapse

Instance Method Details

#check_for_missing_scopes(requested_scopes, body) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/smart_app_launch/token_payload_validation.rb', line 80

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

#validate_fhir_context(fhir_context) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/smart_app_launch/token_payload_validation.rb', line 116

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

#validate_required_fields_present(body, required_fields) ⇒ Object



69
70
71
72
73
74
# File 'lib/smart_app_launch/token_payload_validation.rb', line 69

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

#validate_scope_subset(received_scopes, original_scopes) ⇒ Object



94
95
96
97
98
# File 'lib/smart_app_launch/token_payload_validation.rb', line 94

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

#validate_token_field_types(body) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/smart_app_launch/token_payload_validation.rb', line 100

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

#validate_token_type(body) ⇒ Object



76
77
78
# File 'lib/smart_app_launch/token_payload_validation.rb', line 76

def validate_token_type(body)
  assert body['token_type'].casecmp('bearer').zero?, '`token_type` must be `bearer`'
end