Skip to main content

SMART on FHIR

SMART on FHIR is a standard that enables healthcare applications to securely access patient data across different EHR systems. It builds upon OAuth 2.0 and OpenID Connect to provide healthcare-specific authentication and authorization.

Overview​

SMART on FHIR is an open standard developed by the SMART Health IT project in collaboration with HL7. It allows third-party applications to launch from within EHR systems or standalone, while maintaining consistent security and user experience across different healthcare platforms.

Key Features:

  • EHR Integration: Applications can launch directly from within EHR systems
  • Standardized Scopes: Healthcare-specific permissions using FHIR resource types
  • Patient Context: Applications receive context about which patient's data to access
  • User Context: Applications know which user (patient, practitioner) is authenticated
  • Interoperability: Same app works across different EHR vendors (Epic, Cerner, etc.)

SMART Launch Flows​

SMART on FHIR supports two primary launch patterns:

1. EHR Launch (Contextual Launch)​

The application is launched from within an EHR system, automatically receiving context about the patient and user.

Here Haste Health plays the EHR: a signed-in user starts the launch for a patient (and optionally one of their encounters), and the app is opened at its launch URL. This is how you test a SMART app built for Epic, Oracle Health (Cerner) or MEDITECH EHR launch against Haste Health. Apps built on the SMART JavaScript client (fhirclient) work as they do against those EHRs.

Registering the app​

Register the app as a ClientApplication with a launchUri, the page the app is opened at, and include launch in the scopes it is allowed to request:

{
"resourceType": "ClientApplication",
"id": "smart-app-id",
"name": "My SMART app",
"grantType": ["authorization_code", "refresh_token"],
"responseTypes": "code",
"launchUri": "https://app.example.com/launch",
"redirectUri": ["https://app.example.com/callback"],
"scope": "openid fhirUser launch launch/encounter patient/*.rs offline_access"
}

launchUri is where an EHR launch starts. redirectUri is where authorization ends: they are usually different pages of the app.

Starting a launch​

Invoke $launch on the app's ClientApplication with the patient, and optionally an encounter of that patient:

POST /w/{tenant}/{project}/api/v1/fhir/ClientApplication/smart-app-id/$launch
Authorization: Bearer USER_TOKEN
Content-Type: application/fhir+json

{
"resourceType": "Parameters",
"parameter": [
{ "name": "patient", "valueReference": { "reference": "Patient/123" } },
{ "name": "encounter", "valueReference": { "reference": "Encounter/456" } }
]
}
{
"resourceType": "Parameters",
"parameter": [
{ "name": "launch", "valueString": "q4k1v9yo2x7bt0l8c5m3r6f1-p7w2h9e4d0s5n8j3a6" },
{ "name": "iss", "valueUri": "https://api.haste.health/w/{tenant}/{project}/api/v1/fhir" },
{ "name": "url", "valueUri": "https://app.example.com/launch?iss=https%3A%2F%2Fapi.haste.health%2Fw%2F%7Btenant%7D%2F%7Bproject%7D%2Fapi%2Fv1%2Ffhir&launch=q4k1v9yo2x7bt0l8c5m3r6f1-p7w2h9e4d0s5n8j3a6" }
]
}

Open url in the browser the user is signed in with.

  • Only a signed-in user can start a launch, not a client on its own: the same user then authorizes the app. The patient and encounter are read as that user, so their scopes and access policies decide what they can launch for.
  • The encounter must belong to the patient.
  • A launch expires five minutes after it is created. It can be used more than once within that time.

Authorization and token​

The app passes launch back with the launch scope:

GET /w/{tenant}/{project}/api/v1/oidc/auth/authorize?
response_type=code&
client_id=smart-app-id&
redirect_uri=https://app.example.com/callback&
scope=openid fhirUser launch patient/*.rs&
state=random-state&
aud=https://api.haste.health/w/{tenant}/{project}/api/v1/fhir&
launch=q4k1v9yo2x7bt0l8c5m3r6f1-p7w2h9e4d0s5n8j3a6&
code_challenge=CHALLENGE&
code_challenge_method=S256
  • The launch must be one the signed-in user started for this app, and must not have expired. Otherwise authorization fails with invalid_request.
  • Without the launch scope the launch parameter is ignored. With the scope but no launch value, the token has no launch context and launch is not granted.
  • The token response carries patient, encounter (when the launch has one) and need_patient_banner. patient/ scopes are bound to the launch's patient, and refreshed tokens keep the same context.
  • A user who is themselves linked to a patient can only launch an app for that patient.

2. Standalone Launch​

The application launches independently, and the user selects which patient's data to access.

No patient picker

Haste Health does not show a patient picker. With launch/patient, a standalone launch gets a patient only when the signed-in user is linked to one (through their Membership), as in a patient portal. A clinician who needs a patient in context uses an EHR launch.

SMART Scopes​

SMART on FHIR defines a structured scope syntax that combines permissions with FHIR resource types.

Scope Syntax​

<context>/<resource>.<permission>

Components:

  • Context: Who is accessing the data (patient, user, system)
  • Resource: FHIR resource type (Patient, Observation, etc.) or * for all
  • Permission: Access level (read, write, or * for both)

Scope Categories​

1. Clinical Scopes (Patient Context)​

Access data for the currently selected patient:

patient/ scopes are read and search only

A patient/ scope is bound to the patient in context, from an EHR launch or the signed-in user's own patient link, and grants read and search within that patient's compartment. Write permissions in a patient/ scope are dropped from the token: use user/ scopes to write.

ScopeDescription
patient/*.readRead all patient compartment resources
patient/*.writeWrite all patient compartment resources
patient/*.*Full read/write access to patient compartment
patient/Patient.readRead Patient demographics
patient/Observation.readRead Observations (labs, vitals, etc.)
patient/Condition.readRead Conditions (diagnoses)
patient/MedicationRequest.readRead medication orders
patient/AllergyIntolerance.readRead allergies
patient/Immunization.readRead immunization history
patient/DiagnosticReport.readRead diagnostic reports
patient/Procedure.readRead procedures
patient/DocumentReference.readRead clinical documents
patient/CarePlan.readRead care plans
patient/Goal.readRead patient goals
patient/Encounter.readRead encounter history

2. User-Level Scopes​

Access data across all patients the user has access to:

ScopeDescription
user/*.readRead all resources the user can access
user/*.writeWrite all resources the user can access
user/*.*Full read/write access for the user
user/Patient.readRead all patients user can access
user/Observation.readRead all observations user can access
user/Practitioner.readRead practitioner information
user/Organization.readRead organization information

3. System-Level Scopes​

Access for backend services (Client Credentials flow):

ScopeDescription
system/*.readRead all resources (backend access)
system/*.writeWrite all resources (backend access)
system/*.*Full read/write system access
system/Patient.readRead all patients (no user context)
system/Observation.readRead all observations (no user context)

4. Launch Scopes​

Control launch context and behavior:

ScopeDescription
launchReceive the patient and encounter of an EHR launch
launch/patientA patient in context. In a standalone launch, only when the signed-in user is linked to a patient: there is no picker
launch/encounterThe encounter of an EHR launch started with one
online_accessRequest refresh token for long-term access
offline_accessRequest refresh token for offline/background access

5. OpenID Connect Scopes​

Standard OIDC scopes for user identity:

ScopeDescription
openidRequired for OIDC authentication
profileUser's name, picture, etc.
emailUser's email address
fhirUserFHIR resource representing the user (Patient, Practitioner, etc.)

Token Response with Context​

When using SMART on FHIR, the token response includes additional context information:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "openid fhirUser launch launch/encounter patient/*.rs offline_access",
"patient": "123",
"encounter": "456",
"need_patient_banner": true,
"refresh_token": "...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Context Parameters:

ParameterDescriptionExample
patientId of the Patient in context: the EHR launch's patient, or the signed-in user's own123
encounterId of the Encounter an EHR launch was started in456
need_patient_bannerReturned for an EHR launch, always true: Haste Health opens the app on its own, with no patient banner around ittrue

The authenticated user's FHIR resource (fhirUser, for example Practitioner/789) is a claim in the id_token, not a token response parameter. fhirContext, intent, smart_style_url and tenant are not returned.

SMART Configuration Discovery​

Applications discover SMART capabilities via the .well-known endpoint, scoped to a tenant and project:

Request:

GET /w/{tenant}/{project}/api/v1/fhir/.well-known/smart-configuration HTTP/1.1
Host: api.haste.health

Response:

{
"issuer": "https://api.haste.health",
"authorization_endpoint": "https://api.haste.health/w/{tenant}/{project}/api/v1/oidc/auth/authorize",
"token_endpoint": "https://api.haste.health/w/{tenant}/{project}/api/v1/oidc/auth/token",
"introspection_endpoint": "https://api.haste.health/w/{tenant}/{project}/api/v1/oidc/auth/introspect",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
],
"scopes_supported": [
"openid",
"profile",
"email",
"offline_access",
"fhirUser",
"launch",
"launch/patient",
"launch/encounter",
"patient/*.rs",
"user/*.cruds",
"system/*.cruds"
],
"response_types_supported": [
"code",
"id_token",
"id_token token"
],
"capabilities": [
"sso-openid-connect",
"client-public",
"client-confidential-symmetric",
"launch-standalone",
"launch-ehr",
"context-ehr-patient",
"context-ehr-encounter",
"permission-offline",
"permission-patient",
"permission-user",
"permission-v2"
],
"code_challenge_methods_supported": [
"S256"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token"
]
}

registration_endpoint is omitted entirely (dynamic client registration isn't supported — register a ClientApplication resource directly instead; see Client Registration). For introspection_endpoint, see Token Introspection.

SMART Capabilities​

Supported Capabilities​

Haste Health advertises exactly these SMART capabilities:

CapabilityDescriptionSupported
sso-openid-connectOpenID Connect on top of OAuth 2.0✅ Yes
client-publicPublic clients (no secret), protected with PKCE✅ Yes
client-confidential-symmetricConfidential clients with a shared secret✅ Yes
launch-standaloneStandalone launch✅ Yes
launch-ehrEHR launch, started with ClientApplication/{id}/$launch✅ Yes
context-ehr-patientThe patient of an EHR launch✅ Yes
context-ehr-encounterThe encounter of an EHR launch✅ Yes
permission-offlineRefresh tokens, requested with offline_access✅ Yes
permission-patientpatient/ scopes (read and search only)✅ Yes
permission-useruser/ scopes✅ Yes
permission-v2SMART v2 scope syntax✅ Yes

Not currently advertised or supported: context-standalone-* (there is no patient picker). If your app depends on any of these, confirm it against the live discovery document for your server rather than assuming.

Common Use Cases​

Patient Portal​

// Patient viewing their own data
scope: 'openid profile patient/Patient.read patient/Observation.read patient/Condition.read patient/MedicationRequest.read online_access'

Clinical Decision Support​

// CDS app launched from EHR
scope: 'launch patient/Patient.read patient/Observation.read patient/Condition.read patient/MedicationRequest.read patient/AllergyIntolerance.read'

Population Health​

// Analytics dashboard for clinicians
scope: 'openid profile user/Patient.read user/Observation.read user/Condition.read user/Encounter.read'

Research Data Export​

// Backend system for de-identified data
scope: 'system/Patient.read system/Observation.read system/Condition.read system/Procedure.read'

Resources​