Your health app reminds you about medications, syncs with your lab results, and even pulls records from different hospitals. It feels effortless, but what’s really happening behind the scenes?
The secret is SMART on FHIR.
This game-changing framework is revolutionizing healthcare app development, yet few truly understand how it works. In this guide, we’ll break it down step by step and show you how to build your own SMART-powered health app. When we’re done, you’ll have the foundation to build your own SMART healthcare app.
What is SMART on FHIR?#
Substitutable Medical Applications and Reusable Technologies or SMART is a framework that provides functionalities to build healthcare apps to function on top of EHRs in a secure and standardized way without the need to reconfigure it for individual EHRs. It is a one-size-fits-all solution. When using SMART on FHIR it combines the authentication layer of SMART with the data standardization of FHIR to create a secure, interoperable way for applications to interact with EHRs, thereby proving the best of both worlds.
The core security features of SMART are that it uses oAuth2 and OpenID Connect to securely access the EHR and provide user credentials back to the SMART application.
SMART provides the following advantages:
- Interoperability - Enables apps to work across different EHRs
- Security & Authorization - Uses industry-standard OAuth 2.0 for secure access
- Patient Access - Patients can connect personal health apps to their medical records
- Clinician-Friendly Apps - Doctors can use third-party tools inside EHRs without extra login steps
- Standardized API Access - Developers don’t have to build separate integrations for each hospital/EHR
SMART uses a few core components such as oAuth, OpenID Connect, well-known discovery endpoints, scope and launch context. Let’s understand each of these in the simplest way possible.
Let’s understand all about oAuth2#
Let’s say application ‘A’ wants to access the resources of application ‘B’ for its own use. Now here’s where oAuth2 comes into play, through which ‘B’ can provide ‘A’ access to its resources, effectively providing authorization.
Here’s an oAuth2 playground to follow along.
- Register the client, and keep the credentials safe until you are done with this testing
- Click on ‘Authorization code’ to begin and you will see a similar code as below
Breaking down the parts of the URL:
- client_id: Identifies your application
- response_type=code: Requests an authorization code (OAuth 2.0 Authorization Code Flow).
- scope=openid profile email: The resources being requested by your application from Epic
- redirect_uri: This is where the user will be sent after authentication
- state=random_string: A unique value to prevent CSRF attacks.
This step tells the oAuth2 app that it needs an authorization code.
- Click on Authorize, type in the credentials, and then check the state in the URL and click ‘It matches’. Now you will be shown a POST request code that takes the authorization code requested in step 2 and requests for the access and refresh token.
As you can see from the screenshot the code is being sent along the POST request.
- This is the final step where application ‘A’ gets its access and refresh token, with which now it can access the resources of application ‘B’. Remember to save these tokens to your local storage for API calls.
What’s OpenID Connect? Why use it after oAuth2?#
OpenID Connect (OIDC) is an authentication protocol that works on top of OAuth 2.0, allowing apps to verify a user’s identity and get their profile information securely. To put it simply:
- OAuth 2.0 = Authorization (Who can access what?)
- OpenID Connect = Authentication (Who is this user?)
Now let’s explore the OpenID connect playground
- Click ‘Authorize’ and you’ll be shown the Authorization URL as below. The URL parts are the same as the one seen in oAuth2 except for an addition of ‘Nonce’ which prevents replay attacks.
- Follow the steps and you will shown the Code as below just like in oAuth2, but the difference lies in the next step.
- Click ‘go’ and you will see a token generation response that now contains an additional token ‘id_token’. This is the identity of the user, let’s decode it. Copy the id_token and follow along.
- Navigate to jwt.io where we can decode JWT tokens and paste your token in the box and you’ll see a decoded user detail something like this
This is the sole purpose of OpenID Connect, it gives the details of the authorizing user.
Looking at oAuth2 and OpenID Connect together#
- Authorization Request: Redirect the patient to the EHR’s auth server with response_type=code.
- Patient Authentication (OIDC): The user logs in via the EHR’s identity provider.
- Patient Consent: User grants access to requested resources.
- Authorization Code Issued: EHR redirects back with code=AUTHORIZATION_CODE.
- Token Exchange: The App sends the auth code to EHR’s token endpoint, and gets access_token & id_token.
- Fetch FHIR Data: Use access_token in API requests to access patient data.
- Decode ID Token: Extract user identity (sub, fhirUser, profile).
SMART discovery endpoints and ‘.well-known’#
Discovery Endpoints are used by client apps to dynamically discover authentication The ‘.well-known’ directory defines a location where publicly accessible metadata can be stored in a predictable format. In SMART it is used for auto-discovery of an authorization server’s capabilities.
Let’s use the R4 endpoint https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/
and append .well-known/smart-configurations
and use it in POSTMAN to see the response.
Let’s break down the response:
- authorization_endpoint: Where clients start the OAuth flow (users log in here).
- token_endpoint: Where the client exchanges the authorization code for tokens.
- token_endpoint_auth_methods_supported: How clients authenticate (client_secret_basic, private_key_jwt).
- scopes_supported: List of available (e.g., fhirUser, launch, openid, profile).
- response_types_supported: OAuth response types (e.g., code for Authorization Code Flow).
- grant_types_supported Allowed grant types like authorization_code, client_credentials.
- code_challenge_methods_supported: code challenge methods the authorization server supports for PKCE
Why use discovery endpoints?
- Auto-discovery: Instead of hardcoding URLs, clients can dynamically fetch the OAuth endpoints.
- Security: Ensures clients use the correct authorization and token URLs.
- Interoperability: FHIR apps can integrate with multiple servers using the same discovery logic.
How does this work? When the SMART app starts, it fetches all endpoints available at the .well-known endpoint and now the app has the endpoints to query for different purposes. Hence based on the response breakdown we saw earlier, it sends requests to the respective endpoint.
Let’s look at what scopes are and what they do#
In SMART on FHIR, scopes control access to healthcare data and identity information. Scopes can be simplified in explanation as to what the SMART app needs for it to perform its functions, e.g. requesting user details or patient information.
Some common scopes seen in the R4 endpoint are:
Let’s look at fhirUser scope in depth as that is very important, and we advise you to go through the link for a comprehensive understanding as scopes are a vast topic, whereas launch will be discussed in the next section.
fhirUser scope is divided into:
Patient-specific scopes: Patient-level scopes in SMART on FHIR define what data a healthcare app can access for a specific patient after authentication. They restrict access to only the authorized patient’s data, rather than system-wide or practitioner-wide data.
User-level scopes: User-level scopes allow an app to access multiple data based on the permissions of the logged-in user
System-level scopes: System-level scopes allow server-to-server communication without a user logging in. Used for background tasks, data synchronization, population health analytics, and reporting.
Let’s look at what the Launch context is#
The launch context is an important concept that refers to additional information passed along with the authorization request. It is required by the SMART app to contextually know what data it will be working on.
Eg: A vitals app needs to know which patient is being read and the patient’s observation, hence the launch context here would ask for ‘launch/patient’ to get the patient detail and to read it will raise a scope as user/Observation.read
. This is how scope and launch context work together
Use of Launch Context: When your app is launched, especially in clinical settings, the launch context allows the app to be aware of:
- Which patient the app is focused on?
- Which encounter or episode of care is relevant?
- Which FHIR server should the app interact with?
A quick recap of Scope and Launch Context:#
- Scopes define what data an app can access.
Example:
patient/Observation.read
allows the app to read patient observations. - Launch context provides specific identifiers to the app at runtime.
Example:
launch/patient
gives the app the active patient ID. - Scopes control access to FHIR resources, while launch context provides context.
Example:
patient/Observation.read
allows reading observations, but without launch/patient, the app wouldn’t know which patient to request data for. - Launch context is set during app registration, while scopes are also set but determine access permissions.
- The access token includes both scope permissions and launch context parameters.
- The app can only use the data granted by the combination of scope and launch context.
Example: If an app has
patient/Observation.read
but nolaunch/patient
, it cannot automatically get patient data without user input. - If an app needs more context or permissions, both the requested scopes and launch parameters must be updated in the registration.
Conclusion#
Now that we’ve covered the fundamentals—authorization with OAuth and authentication using OpenID Connect—it’s time to dive into the code. Here’s the step-by-step guide to get started.
If you still have questions, feel free to revisit the earlier sections. These core concepts are crucial for understanding and successfully implementing the next steps.