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
launchscope thelaunchparameter is ignored. With the scope but nolaunchvalue, the token has no launch context andlaunchis not granted. - The token response carries
patient,encounter(when the launch has one) andneed_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.
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 onlyA 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.
| Scope | Description |
|---|---|
patient/*.read | Read all patient compartment resources |
patient/*.write | Write all patient compartment resources |
patient/*.* | Full read/write access to patient compartment |
patient/Patient.read | Read Patient demographics |
patient/Observation.read | Read Observations (labs, vitals, etc.) |
patient/Condition.read | Read Conditions (diagnoses) |
patient/MedicationRequest.read | Read medication orders |
patient/AllergyIntolerance.read | Read allergies |
patient/Immunization.read | Read immunization history |
patient/DiagnosticReport.read | Read diagnostic reports |
patient/Procedure.read | Read procedures |
patient/DocumentReference.read | Read clinical documents |
patient/CarePlan.read | Read care plans |
patient/Goal.read | Read patient goals |
patient/Encounter.read | Read encounter history |
2. User-Level Scopes
Access data across all patients the user has access to:
| Scope | Description |
|---|---|
user/*.read | Read all resources the user can access |
user/*.write | Write all resources the user can access |
user/*.* | Full read/write access for the user |
user/Patient.read | Read all patients user can access |
user/Observation.read | Read all observations user can access |
user/Practitioner.read | Read practitioner information |
user/Organization.read | Read organization information |
3. System-Level Scopes
Access for backend services (Client Credentials flow):
| Scope | Description |
|---|---|
system/*.read | Read all resources (backend access) |
system/*.write | Write all resources (backend access) |
system/*.* | Full read/write system access |
system/Patient.read | Read all patients (no user context) |
system/Observation.read | Read all observations (no user context) |
4. Launch Scopes
Control launch context and behavior:
| Scope | Description |
|---|---|
launch | Receive the patient and encounter of an EHR launch |
launch/patient | A patient in context. In a standalone launch, only when the signed-in user is linked to a patient: there is no picker |
launch/encounter | The encounter of an EHR launch started with one |
online_access | Request refresh token for long-term access |
offline_access | Request refresh token for offline/background access |
5. OpenID Connect Scopes
Standard OIDC scopes for user identity:
| Scope | Description |
|---|---|
openid | Required for OIDC authentication |
profile | User's name, picture, etc. |
email | User's email address |
fhirUser | FHIR 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:
| Parameter | Description | Example |
|---|---|---|
patient | Id of the Patient in context: the EHR launch's patient, or the signed-in user's own | 123 |
encounter | Id of the Encounter an EHR launch was started in | 456 |
need_patient_banner | Returned for an EHR launch, always true: Haste Health opens the app on its own, with no patient banner around it | true |
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:
| Capability | Description | Supported |
|---|---|---|
sso-openid-connect | OpenID Connect on top of OAuth 2.0 | ✅ Yes |
client-public | Public clients (no secret), protected with PKCE | ✅ Yes |
client-confidential-symmetric | Confidential clients with a shared secret | ✅ Yes |
launch-standalone | Standalone launch | ✅ Yes |
launch-ehr | EHR launch, started with ClientApplication/{id}/$launch | ✅ Yes |
context-ehr-patient | The patient of an EHR launch | ✅ Yes |
context-ehr-encounter | The encounter of an EHR launch | ✅ Yes |
permission-offline | Refresh tokens, requested with offline_access | ✅ Yes |
permission-patient | patient/ scopes (read and search only) | ✅ Yes |
permission-user | user/ scopes | ✅ Yes |
permission-v2 | SMART 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'